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::Revoked => NET_ERR_TOKEN_NOT_AUTHORIZED,
180 CoreTokenError::ReadOnly => NET_ERR_IDENTITY,
185 CoreTokenError::ZeroTtl => NET_ERR_TOKEN_INVALID_FORMAT,
192 CoreTokenError::TtlTooLong => NET_ERR_TOKEN_INVALID_FORMAT,
196 }
197}
198
199fn runtime() -> &'static Arc<Runtime> {
215 use std::sync::OnceLock;
216 static RT: OnceLock<Arc<Runtime>> = OnceLock::new();
217 RT.get_or_init(|| {
218 match tokio::runtime::Builder::new_multi_thread()
219 .enable_all()
220 .build()
221 {
222 Ok(rt) => Arc::new(rt),
223 Err(e) => {
224 eprintln!(
225 "FATAL: mesh FFI tokio runtime build failure ({e:?}); aborting to avoid panic across the FFI boundary"
226 );
227 std::process::abort();
228 }
229 }
230 })
231}
232
233pub(super) fn block_on<F: std::future::Future>(future: F) -> F::Output {
253 if tokio::runtime::Handle::try_current().is_ok() {
254 eprintln!(
255 "FATAL: mesh FFI called from inside a tokio runtime context; \
256 aborting to avoid runtime-in-runtime panic across the FFI boundary"
257 );
258 std::process::abort();
259 }
260 runtime().block_on(future)
261}
262
263#[inline]
286pub(super) unsafe fn c_str_to_string(p: *const c_char) -> Option<String> {
287 if p.is_null() {
288 return None;
289 }
290 CStr::from_ptr(p).to_str().ok().map(str::to_owned)
291}
292
293fn write_json_out<T: Serialize>(
299 value: &T,
300 out_ptr: *mut *mut c_char,
301 out_len: *mut usize,
302) -> c_int {
303 if out_ptr.is_null() || out_len.is_null() {
304 return NetError::NullPointer.into();
305 }
306 let Ok(s) = serde_json::to_string(value) else {
307 return NetError::Unknown.into();
308 };
309 let len = s.len();
310 let Ok(cs) = CString::new(s) else {
311 return NetError::Unknown.into();
312 };
313 unsafe {
314 *out_ptr = cs.into_raw();
315 *out_len = len;
316 }
317 0
318}
319
320pub(super) fn write_string_out(s: String, out_ptr: *mut *mut c_char, out_len: *mut usize) -> c_int {
321 if out_ptr.is_null() || out_len.is_null() {
322 return NetError::NullPointer.into();
323 }
324 let len = s.len();
325 let Ok(cs) = CString::new(s) else {
326 return NetError::Unknown.into();
327 };
328 unsafe {
329 *out_ptr = cs.into_raw();
330 *out_len = len;
331 }
332 0
333}
334
335fn adapter_err_to_code(err: &AdapterError) -> c_int {
336 match err {
337 AdapterError::Connection(_) => NET_ERR_MESH_HANDSHAKE,
338 _ => NET_ERR_MESH_TRANSPORT,
339 }
340}
341
342fn stream_err_to_code(err: &StreamError) -> c_int {
343 match err {
344 StreamError::Backpressure => NET_ERR_MESH_BACKPRESSURE,
345 StreamError::NotConnected => NET_ERR_MESH_NOT_CONNECTED,
346 StreamError::Transport(_) => NET_ERR_MESH_TRANSPORT,
347 }
348}
349
350#[derive(Deserialize)]
355struct SubnetPolicyJson {
356 #[serde(default)]
357 rules: Vec<SubnetRuleJson>,
358}
359
360#[derive(Deserialize)]
361struct SubnetRuleJson {
362 tag_prefix: String,
363 level: u32,
364 #[serde(default)]
365 values: std::collections::HashMap<String, u32>,
366}
367
368fn u8_from_u32(value: u32) -> Option<u8> {
369 if value > 255 {
370 None
371 } else {
372 Some(value as u8)
373 }
374}
375
376fn subnet_id_from_json(levels: Vec<u32>) -> Option<SubnetId> {
377 if levels.is_empty() || levels.len() > 4 {
378 return None;
379 }
380 let mut bytes = [0u8; 4];
381 for (i, raw) in levels.iter().enumerate() {
382 bytes[i] = u8_from_u32(*raw)?;
383 }
384 Some(SubnetId::new(&bytes[..levels.len()]))
385}
386
387fn subnet_policy_from_json(p: SubnetPolicyJson) -> Option<SubnetPolicy> {
388 let mut policy = SubnetPolicy::new();
389 for rule_json in p.rules {
390 let level = u8_from_u32(rule_json.level)?;
391 if level > 3 {
392 return None;
393 }
394 let mut rule = SubnetRule::new(rule_json.tag_prefix, level);
395 for (tag_value, raw_val) in rule_json.values {
396 let v = u8_from_u32(raw_val)?;
397 if v == 0 {
403 return None;
404 }
405 rule = rule.map(tag_value, v);
406 }
407 policy = policy.add_rule(rule);
408 }
409 Some(policy)
410}
411
412#[derive(Deserialize)]
413struct MeshNewConfig {
414 bind_addr: String,
415 psk_hex: String,
417 heartbeat_ms: Option<u64>,
418 session_timeout_ms: Option<u64>,
419 num_shards: Option<u16>,
420 capability_gc_interval_ms: Option<u64>,
423 require_signed_capabilities: Option<bool>,
426 subnet: Option<Vec<u32>>,
428 subnet_policy: Option<SubnetPolicyJson>,
430 identity_seed_hex: Option<String>,
435 #[serde(default)]
441 reflex_override: Option<String>,
442 #[serde(default)]
446 try_port_mapping: bool,
447 #[serde(default)]
453 auto_direct_upgrade: bool,
454}
455
456pub struct MeshNodeHandle {
469 inner: ManuallyDrop<Arc<MeshNode>>,
470 channel_configs: ManuallyDrop<Arc<ChannelConfigRegistry>>,
471 guard: HandleGuard,
472}
473
474#[unsafe(no_mangle)]
489pub unsafe extern "C" fn net_mesh_new(
490 config_json: *const c_char,
491 out_handle: *mut *mut MeshNodeHandle,
492) -> c_int {
493 if config_json.is_null() || out_handle.is_null() {
494 return NetError::NullPointer.into();
495 }
496 let Some(s) = (unsafe { c_str_to_string(config_json) }) else {
497 return NetError::InvalidUtf8.into();
498 };
499 let cfg: MeshNewConfig = match serde_json::from_str(&s) {
500 Ok(v) => v,
501 Err(_) => return NetError::InvalidJson.into(),
502 };
503 let bind_addr: std::net::SocketAddr = match cfg.bind_addr.parse() {
504 Ok(a) => a,
505 Err(_) => return NET_ERR_MESH_INIT,
506 };
507 let psk_bytes = match hex::decode(&cfg.psk_hex) {
508 Ok(b) => b,
509 Err(_) => return NET_ERR_MESH_INIT,
510 };
511 if psk_bytes.len() != 32 {
512 return NET_ERR_MESH_INIT;
513 }
514 let mut psk = [0u8; 32];
515 psk.copy_from_slice(&psk_bytes);
516
517 let mut node_cfg = MeshNodeConfig::new(bind_addr, psk);
518 if let Some(ms) = cfg.heartbeat_ms {
526 if ms == 0 {
527 return NetError::InvalidJson.into();
528 }
529 node_cfg = node_cfg.with_heartbeat_interval(std::time::Duration::from_millis(ms));
530 }
531 if let Some(ms) = cfg.session_timeout_ms {
532 if ms == 0 {
533 return NetError::InvalidJson.into();
534 }
535 node_cfg = node_cfg.with_session_timeout(std::time::Duration::from_millis(ms));
536 }
537 if let Some(n) = cfg.num_shards {
538 node_cfg = node_cfg.with_num_shards(n);
539 }
540 if let Some(ms) = cfg.capability_gc_interval_ms {
541 node_cfg = node_cfg.with_capability_gc_interval(std::time::Duration::from_millis(ms));
542 }
543 if let Some(b) = cfg.require_signed_capabilities {
544 node_cfg = node_cfg.with_require_signed_capabilities(b);
545 }
546 if let Some(levels) = cfg.subnet {
547 let Some(id) = subnet_id_from_json(levels) else {
548 return NET_ERR_MESH_INIT;
549 };
550 node_cfg = node_cfg.with_subnet(id);
551 }
552 if let Some(policy_js) = cfg.subnet_policy {
553 let Some(policy) = subnet_policy_from_json(policy_js) else {
554 return NET_ERR_MESH_INIT;
555 };
556 node_cfg = node_cfg.with_subnet_policy(Arc::new(policy));
557 }
558 #[cfg(feature = "nat-traversal")]
559 if let Some(external_str) = cfg.reflex_override.as_deref() {
560 let Ok(external) = external_str.parse::<std::net::SocketAddr>() else {
561 return NET_ERR_MESH_INIT;
562 };
563 node_cfg = node_cfg.with_reflex_override(external);
564 }
565 #[cfg(not(feature = "nat-traversal"))]
569 let _ = cfg.reflex_override;
570 #[cfg(feature = "port-mapping")]
571 if cfg.try_port_mapping {
572 node_cfg = node_cfg.with_try_port_mapping(true);
573 }
574 #[cfg(not(feature = "port-mapping"))]
576 let _ = cfg.try_port_mapping;
577 #[cfg(feature = "nat-traversal")]
578 if cfg.auto_direct_upgrade {
579 node_cfg = node_cfg.with_auto_direct_upgrade(true);
580 }
581 #[cfg(not(feature = "nat-traversal"))]
583 let _ = cfg.auto_direct_upgrade;
584
585 let identity = match cfg.identity_seed_hex {
586 Some(seed_hex) => {
587 let bytes = match hex::decode(&seed_hex) {
588 Ok(b) => b,
589 Err(_) => return NET_ERR_MESH_INIT,
590 };
591 if bytes.len() != 32 {
592 return NET_ERR_MESH_INIT;
593 }
594 let mut arr = [0u8; 32];
595 arr.copy_from_slice(&bytes);
596 EntityKeypair::from_bytes(arr)
597 }
598 None => EntityKeypair::generate(),
599 };
600 let result = block_on(async move { MeshNode::new(identity, node_cfg).await });
601 match result {
602 Ok(mut node) => {
603 let channel_configs = Arc::new(ChannelConfigRegistry::new());
604 node.set_channel_configs(channel_configs.clone());
605 node.set_token_cache(Arc::new(TokenCache::new()));
609 let handle = Box::new(MeshNodeHandle {
610 inner: ManuallyDrop::new(Arc::new(node)),
611 channel_configs: ManuallyDrop::new(channel_configs),
612 guard: HandleGuard::new(),
613 });
614 unsafe {
615 *out_handle = Box::into_raw(handle);
616 }
617 0
618 }
619 Err(_) => NET_ERR_MESH_INIT,
620 }
621}
622
623#[unsafe(no_mangle)]
624pub unsafe extern "C" fn net_mesh_free(handle: *mut MeshNodeHandle) {
625 if handle.is_null() {
626 return;
627 }
628 let h: &MeshNodeHandle = unsafe { &*handle };
633 if h.guard.begin_free(FFI_HANDLE_FREE_DEADLINE) {
634 unsafe {
636 let mh = &mut *handle;
637 let inner = ManuallyDrop::take(&mut mh.inner);
638 let configs = ManuallyDrop::take(&mut mh.channel_configs);
639 drop(inner);
640 drop(configs);
641 }
642 } else {
643 tracing::warn!(
644 "net_mesh_free: in-flight ops did not drain within deadline; \
645 leaking inner to avoid use-after-free"
646 );
647 }
648}
649
650#[cfg(any(feature = "cortex", feature = "dataforts"))]
668pub(super) fn mesh_node_arc(h: &MeshNodeHandle) -> Option<Arc<MeshNode>> {
669 let _op = h.guard.try_enter()?;
670 Some(Arc::clone(&h.inner))
671}
672
673#[unsafe(no_mangle)]
681pub unsafe extern "C" fn net_mesh_arc_clone(handle: *mut MeshNodeHandle) -> *mut Arc<MeshNode> {
682 if handle.is_null() {
683 return std::ptr::null_mut();
684 }
685 let h = unsafe { &*handle };
686 let _op = match h.guard.try_enter() {
688 Some(op) => op,
689 None => return std::ptr::null_mut(),
690 };
691 let cloned: Arc<MeshNode> = Arc::clone(&h.inner);
692 Box::into_raw(Box::new(cloned))
693}
694
695#[unsafe(no_mangle)]
702pub unsafe extern "C" fn net_mesh_channel_configs_arc_clone(
703 handle: *mut MeshNodeHandle,
704) -> *mut Arc<ChannelConfigRegistry> {
705 if handle.is_null() {
706 return std::ptr::null_mut();
707 }
708 let h = unsafe { &*handle };
709 let _op = match h.guard.try_enter() {
711 Some(op) => op,
712 None => return std::ptr::null_mut(),
713 };
714 let cloned: Arc<ChannelConfigRegistry> = Arc::clone(&h.channel_configs);
715 Box::into_raw(Box::new(cloned))
716}
717
718#[unsafe(no_mangle)]
721pub unsafe extern "C" fn net_mesh_arc_free(p: *mut Arc<MeshNode>) {
722 if p.is_null() {
723 return;
724 }
725 unsafe {
726 drop(Box::from_raw(p));
727 }
728}
729
730#[unsafe(no_mangle)]
733pub unsafe extern "C" fn net_mesh_channel_configs_arc_free(p: *mut Arc<ChannelConfigRegistry>) {
734 if p.is_null() {
735 return;
736 }
737 unsafe {
738 drop(Box::from_raw(p));
739 }
740}
741
742#[unsafe(no_mangle)]
745pub unsafe extern "C" fn net_mesh_public_key_hex(
746 handle: *mut MeshNodeHandle,
747 out_ptr: *mut *mut c_char,
748 out_len: *mut usize,
749) -> c_int {
750 if handle.is_null() || out_ptr.is_null() || out_len.is_null() {
751 return NetError::NullPointer.into();
752 }
753 let h = unsafe { &*handle };
754 let _op = match h.guard.try_enter() {
755 Some(op) => op,
756 None => return NetError::ShuttingDown.into(),
757 };
758 let s = hex::encode(h.inner.public_key());
759 write_string_out(s, out_ptr, out_len)
760}
761
762#[unsafe(no_mangle)]
763pub unsafe extern "C" fn net_mesh_node_id(handle: *mut MeshNodeHandle) -> u64 {
764 if handle.is_null() {
765 return 0;
766 }
767 let h = unsafe { &*handle };
768 let _op = match h.guard.try_enter() {
770 Some(op) => op,
771 None => return 0,
772 };
773 h.inner.node_id()
774}
775
776#[unsafe(no_mangle)]
780pub unsafe extern "C" fn net_mesh_entity_id(handle: *mut MeshNodeHandle, out: *mut u8) -> c_int {
781 if handle.is_null() || out.is_null() {
782 return NetError::NullPointer.into();
783 }
784 let h = unsafe { &*handle };
785 let _op = match h.guard.try_enter() {
786 Some(op) => op,
787 None => return NetError::ShuttingDown.into(),
788 };
789 let bytes = h.inner.entity_id().as_bytes();
790 unsafe {
791 std::ptr::copy_nonoverlapping(bytes.as_ptr(), out, 32);
792 }
793 0
794}
795unsafe fn parse_peer_pubkey_hex(peer_pubkey_hex: *const c_char) -> Result<[u8; 32], c_int> {
808 let Some(pk_s) = (unsafe { c_str_to_string(peer_pubkey_hex) }) else {
809 return Err(NetError::InvalidUtf8.into());
810 };
811 let pk_bytes = match hex::decode(pk_s) {
812 Ok(b) => b,
813 Err(_) => return Err(NET_ERR_MESH_HANDSHAKE),
814 };
815 if pk_bytes.len() != 32 {
816 return Err(NET_ERR_MESH_HANDSHAKE);
817 }
818 let mut pk = [0u8; 32];
819 pk.copy_from_slice(&pk_bytes);
820 Ok(pk)
821}
822
823#[unsafe(no_mangle)]
825pub unsafe extern "C" fn net_mesh_connect(
826 handle: *mut MeshNodeHandle,
827 peer_addr: *const c_char,
828 peer_pubkey_hex: *const c_char,
829 peer_node_id: u64,
830) -> c_int {
831 if handle.is_null() || peer_addr.is_null() || peer_pubkey_hex.is_null() {
832 return NetError::NullPointer.into();
833 }
834 let h = unsafe { &*handle };
835 let _op = match h.guard.try_enter() {
836 Some(op) => op,
837 None => return NetError::ShuttingDown.into(),
838 };
839 let Some(addr_s) = (unsafe { c_str_to_string(peer_addr) }) else {
840 return NetError::InvalidUtf8.into();
841 };
842 let addr: std::net::SocketAddr = match addr_s.parse() {
843 Ok(a) => a,
844 Err(_) => return NET_ERR_MESH_HANDSHAKE,
845 };
846 let pk = match unsafe { parse_peer_pubkey_hex(peer_pubkey_hex) } {
847 Ok(pk) => pk,
848 Err(code) => return code,
849 };
850
851 let node = h.inner.clone();
852 match block_on(async move { node.connect(addr, &pk, peer_node_id).await }) {
853 Ok(_) => 0,
854 Err(e) => adapter_err_to_code(&e),
855 }
856}
857
858#[unsafe(no_mangle)]
861pub unsafe extern "C" fn net_mesh_accept(
862 handle: *mut MeshNodeHandle,
863 peer_node_id: u64,
864 out_addr: *mut *mut c_char,
865 out_len: *mut usize,
866) -> c_int {
867 if handle.is_null() || out_addr.is_null() || out_len.is_null() {
868 return NetError::NullPointer.into();
869 }
870 let h = unsafe { &*handle };
871 let _op = match h.guard.try_enter() {
872 Some(op) => op,
873 None => return NetError::ShuttingDown.into(),
874 };
875 let node = h.inner.clone();
876 match block_on(async move { node.accept(peer_node_id).await }) {
877 Ok((addr, _)) => write_string_out(addr.to_string(), out_addr, out_len),
878 Err(e) => adapter_err_to_code(&e),
879 }
880}
881
882#[unsafe(no_mangle)]
883pub unsafe extern "C" fn net_mesh_start(handle: *mut MeshNodeHandle) -> c_int {
884 if handle.is_null() {
885 return NetError::NullPointer.into();
886 }
887 let h = unsafe { &*handle };
888 let _op = match h.guard.try_enter() {
889 Some(op) => op,
890 None => return NetError::ShuttingDown.into(),
891 };
892 let node = h.inner.clone();
893 block_on(async move { node.start_arc() });
897 0
898}
899
900#[unsafe(no_mangle)]
912pub unsafe extern "C" fn net_mesh_shutdown(handle: *mut MeshNodeHandle) -> c_int {
913 if handle.is_null() {
914 return NetError::NullPointer.into();
915 }
916 let h = unsafe { &*handle };
917 let _op = match h.guard.try_enter() {
918 Some(op) => op,
919 None => return NetError::ShuttingDown.into(),
920 };
921 match block_on(async { h.inner.shutdown().await }) {
922 Ok(()) => 0,
923 Err(e) => adapter_err_to_code(&e),
924 }
925}
926
927#[cfg(feature = "nat-traversal")]
949#[unsafe(no_mangle)]
950pub unsafe extern "C" fn net_mesh_nat_type(
951 handle: *mut MeshNodeHandle,
952 out_str: *mut *mut c_char,
953 out_len: *mut usize,
954) -> c_int {
955 if handle.is_null() || out_str.is_null() || out_len.is_null() {
956 return NetError::NullPointer.into();
957 }
958 let h = unsafe { &*handle };
959 let _op = match h.guard.try_enter() {
960 Some(op) => op,
961 None => return NetError::ShuttingDown.into(),
962 };
963 write_string_out(
964 nat_class_to_str(h.inner.nat_class()).to_string(),
965 out_str,
966 out_len,
967 )
968}
969
970#[cfg(feature = "nat-traversal")]
975#[unsafe(no_mangle)]
976pub unsafe extern "C" fn net_mesh_reflex_addr(
977 handle: *mut MeshNodeHandle,
978 out_str: *mut *mut c_char,
979 out_len: *mut usize,
980) -> c_int {
981 if handle.is_null() || out_str.is_null() || out_len.is_null() {
982 return NetError::NullPointer.into();
983 }
984 let h = unsafe { &*handle };
985 let _op = match h.guard.try_enter() {
986 Some(op) => op,
987 None => return NetError::ShuttingDown.into(),
988 };
989 let s = h
990 .inner
991 .reflex_addr()
992 .map(|a| a.to_string())
993 .unwrap_or_default();
994 write_string_out(s, out_str, out_len)
995}
996
997#[cfg(feature = "nat-traversal")]
1001#[unsafe(no_mangle)]
1002pub unsafe extern "C" fn net_mesh_peer_nat_type(
1003 handle: *mut MeshNodeHandle,
1004 peer_node_id: u64,
1005 out_str: *mut *mut c_char,
1006 out_len: *mut usize,
1007) -> c_int {
1008 if handle.is_null() || out_str.is_null() || out_len.is_null() {
1009 return NetError::NullPointer.into();
1010 }
1011 let h = unsafe { &*handle };
1012 let _op = match h.guard.try_enter() {
1013 Some(op) => op,
1014 None => return NetError::ShuttingDown.into(),
1015 };
1016 write_string_out(
1017 nat_class_to_str(h.inner.peer_nat_class(peer_node_id)).to_string(),
1018 out_str,
1019 out_len,
1020 )
1021}
1022
1023#[cfg(feature = "nat-traversal")]
1032#[unsafe(no_mangle)]
1033pub unsafe extern "C" fn net_mesh_probe_reflex(
1034 handle: *mut MeshNodeHandle,
1035 peer_node_id: u64,
1036 out_str: *mut *mut c_char,
1037 out_len: *mut usize,
1038) -> c_int {
1039 if handle.is_null() || out_str.is_null() || out_len.is_null() {
1040 return NetError::NullPointer.into();
1041 }
1042 let h = unsafe { &*handle };
1043 let _op = match h.guard.try_enter() {
1044 Some(op) => op,
1045 None => return NetError::ShuttingDown.into(),
1046 };
1047 let node = h.inner.clone();
1048 match block_on(async move { node.probe_reflex(peer_node_id).await }) {
1049 Ok(addr) => write_string_out(addr.to_string(), out_str, out_len),
1050 Err(e) => traversal_err_to_code(&e),
1051 }
1052}
1053
1054#[cfg(feature = "nat-traversal")]
1059#[unsafe(no_mangle)]
1060pub unsafe extern "C" fn net_mesh_reclassify_nat(handle: *mut MeshNodeHandle) -> c_int {
1061 if handle.is_null() {
1062 return NetError::NullPointer.into();
1063 }
1064 let h = unsafe { &*handle };
1065 let _op = match h.guard.try_enter() {
1066 Some(op) => op,
1067 None => return NetError::ShuttingDown.into(),
1068 };
1069 let node = h.inner.clone();
1070 block_on(async move { node.reclassify_nat().await });
1071 0
1072}
1073
1074#[cfg(feature = "nat-traversal")]
1079#[unsafe(no_mangle)]
1080pub unsafe extern "C" fn net_mesh_traversal_stats(
1081 handle: *mut MeshNodeHandle,
1082 out_punches_attempted: *mut u64,
1083 out_punches_succeeded: *mut u64,
1084 out_relay_fallbacks: *mut u64,
1085) -> c_int {
1086 if handle.is_null() {
1087 return NetError::NullPointer.into();
1088 }
1089 let h = unsafe { &*handle };
1090 let _op = match h.guard.try_enter() {
1091 Some(op) => op,
1092 None => return NetError::ShuttingDown.into(),
1093 };
1094 let snap = h.inner.traversal_stats();
1095 unsafe {
1096 if !out_punches_attempted.is_null() {
1097 *out_punches_attempted = snap.punches_attempted;
1098 }
1099 if !out_punches_succeeded.is_null() {
1100 *out_punches_succeeded = snap.punches_succeeded;
1101 }
1102 if !out_relay_fallbacks.is_null() {
1103 *out_relay_fallbacks = snap.relay_fallbacks;
1104 }
1105 }
1106 0
1107}
1108
1109#[cfg(feature = "nat-traversal")]
1121#[unsafe(no_mangle)]
1122pub unsafe extern "C" fn net_mesh_connect_direct(
1123 handle: *mut MeshNodeHandle,
1124 peer_node_id: u64,
1125 peer_pubkey_hex: *const c_char,
1126 coordinator: u64,
1127) -> c_int {
1128 if handle.is_null() || peer_pubkey_hex.is_null() {
1129 return NetError::NullPointer.into();
1130 }
1131 let h = unsafe { &*handle };
1132 let _op = match h.guard.try_enter() {
1133 Some(op) => op,
1134 None => return NetError::ShuttingDown.into(),
1135 };
1136 let pk = match unsafe { parse_peer_pubkey_hex(peer_pubkey_hex) } {
1137 Ok(pk) => pk,
1138 Err(code) => return code,
1139 };
1140
1141 let node = h.inner.clone();
1142 match block_on(async move { node.connect_direct(peer_node_id, &pk, coordinator).await }) {
1143 Ok(_) => 0,
1144 Err(e) => traversal_err_to_code(&e),
1145 }
1146}
1147
1148#[cfg(feature = "nat-traversal")]
1157#[unsafe(no_mangle)]
1158pub unsafe extern "C" fn net_mesh_connect_direct_auto(
1159 handle: *mut MeshNodeHandle,
1160 peer_node_id: u64,
1161 peer_pubkey_hex: *const c_char,
1162) -> c_int {
1163 if handle.is_null() || peer_pubkey_hex.is_null() {
1164 return NetError::NullPointer.into();
1165 }
1166 let h = unsafe { &*handle };
1167 let _op = match h.guard.try_enter() {
1168 Some(op) => op,
1169 None => return NetError::ShuttingDown.into(),
1170 };
1171 let pk = match unsafe { parse_peer_pubkey_hex(peer_pubkey_hex) } {
1172 Ok(pk) => pk,
1173 Err(code) => return code,
1174 };
1175
1176 let node = h.inner.clone();
1177 match block_on(async move { node.connect_direct_auto(peer_node_id, &pk).await }) {
1178 Ok(_) => 0,
1179 Err(e) => traversal_err_to_code(&e),
1180 }
1181}
1182
1183#[repr(C)]
1189pub struct NetTraversalStatsV2 {
1190 pub punches_attempted: u64,
1192 pub punches_succeeded: u64,
1194 pub punches_failed: u64,
1196 pub relay_fallbacks: u64,
1198 pub punch_timeouts: u64,
1200 pub punch_rejections: u64,
1202 pub rendezvous_no_relay: u64,
1204 pub upgrades_attempted: u64,
1206 pub upgrades_succeeded: u64,
1208 pub upgrades_deferred_busy: u64,
1210 pub port_mapping_renewals: u64,
1212 pub port_mapping_active: u8,
1214 pub port_mapping_external: [c_char; 64],
1218}
1219
1220#[cfg(feature = "nat-traversal")]
1224fn fill_traversal_stats_v2(
1225 snap: &crate::adapter::net::traversal::TraversalStatsSnapshot,
1226 out: &mut NetTraversalStatsV2,
1227) {
1228 out.punches_attempted = snap.punches_attempted;
1229 out.punches_succeeded = snap.punches_succeeded;
1230 out.punches_failed = snap.punches_failed;
1231 out.relay_fallbacks = snap.relay_fallbacks;
1232 out.punch_timeouts = snap.punch_timeouts;
1233 out.punch_rejections = snap.punch_rejections;
1234 out.rendezvous_no_relay = snap.rendezvous_no_relay;
1235 out.upgrades_attempted = snap.upgrades_attempted;
1236 out.upgrades_succeeded = snap.upgrades_succeeded;
1237 out.upgrades_deferred_busy = snap.upgrades_deferred_busy;
1238 out.port_mapping_renewals = snap.port_mapping_renewals;
1239 out.port_mapping_active = u8::from(snap.port_mapping_active);
1240 out.port_mapping_external = [0; 64];
1241 if let Some(addr) = snap.port_mapping_external {
1242 let s = addr.to_string();
1243 let n = s.len().min(63);
1248 for (dst, src) in out.port_mapping_external[..n].iter_mut().zip(s.as_bytes()) {
1249 *dst = *src as c_char;
1250 }
1251 }
1252}
1253
1254#[cfg(feature = "nat-traversal")]
1265#[unsafe(no_mangle)]
1266pub unsafe extern "C" fn net_mesh_traversal_stats_v2(
1267 handle: *mut MeshNodeHandle,
1268 out: *mut NetTraversalStatsV2,
1269) -> c_int {
1270 if handle.is_null() || out.is_null() {
1271 return NetError::NullPointer.into();
1272 }
1273 let h = unsafe { &*handle };
1274 let _op = match h.guard.try_enter() {
1275 Some(op) => op,
1276 None => return NetError::ShuttingDown.into(),
1277 };
1278 let snap = h.inner.traversal_stats();
1279 fill_traversal_stats_v2(&snap, unsafe { &mut *out });
1280 0
1281}
1282
1283#[cfg(feature = "nat-traversal")]
1291#[unsafe(no_mangle)]
1292pub unsafe extern "C" fn net_mesh_set_reflex_override(
1293 handle: *mut MeshNodeHandle,
1294 external: *const c_char,
1295) -> c_int {
1296 if handle.is_null() || external.is_null() {
1297 return NetError::NullPointer.into();
1298 }
1299 let h = unsafe { &*handle };
1300 let _op = match h.guard.try_enter() {
1301 Some(op) => op,
1302 None => return NetError::ShuttingDown.into(),
1303 };
1304 let Some(s) = (unsafe { c_str_to_string(external) }) else {
1305 return NetError::InvalidUtf8.into();
1306 };
1307 let Ok(addr) = s.parse::<std::net::SocketAddr>() else {
1308 return NET_ERR_MESH_INIT;
1309 };
1310 h.inner.set_reflex_override(addr);
1311 0
1312}
1313
1314#[cfg(feature = "nat-traversal")]
1322#[unsafe(no_mangle)]
1323pub unsafe extern "C" fn net_mesh_clear_reflex_override(handle: *mut MeshNodeHandle) -> c_int {
1324 if handle.is_null() {
1325 return NetError::NullPointer.into();
1326 }
1327 let h = unsafe { &*handle };
1328 let _op = match h.guard.try_enter() {
1329 Some(op) => op,
1330 None => return NetError::ShuttingDown.into(),
1331 };
1332 h.inner.clear_reflex_override();
1333 0
1334}
1335
1336#[cfg(not(feature = "nat-traversal"))]
1359#[unsafe(no_mangle)]
1360pub unsafe extern "C" fn net_mesh_nat_type(
1361 _handle: *mut MeshNodeHandle,
1362 _out_str: *mut *mut c_char,
1363 _out_len: *mut usize,
1364) -> c_int {
1365 NET_ERR_TRAVERSAL_UNSUPPORTED
1366}
1367
1368#[cfg(not(feature = "nat-traversal"))]
1369#[unsafe(no_mangle)]
1370pub unsafe extern "C" fn net_mesh_reflex_addr(
1371 _handle: *mut MeshNodeHandle,
1372 _out_str: *mut *mut c_char,
1373 _out_len: *mut usize,
1374) -> c_int {
1375 NET_ERR_TRAVERSAL_UNSUPPORTED
1376}
1377
1378#[cfg(not(feature = "nat-traversal"))]
1379#[unsafe(no_mangle)]
1380pub unsafe extern "C" fn net_mesh_peer_nat_type(
1381 _handle: *mut MeshNodeHandle,
1382 _peer_node_id: u64,
1383 _out_str: *mut *mut c_char,
1384 _out_len: *mut usize,
1385) -> c_int {
1386 NET_ERR_TRAVERSAL_UNSUPPORTED
1387}
1388
1389#[cfg(not(feature = "nat-traversal"))]
1390#[unsafe(no_mangle)]
1391pub unsafe extern "C" fn net_mesh_probe_reflex(
1392 _handle: *mut MeshNodeHandle,
1393 _peer_node_id: u64,
1394 _out_str: *mut *mut c_char,
1395 _out_len: *mut usize,
1396) -> c_int {
1397 NET_ERR_TRAVERSAL_UNSUPPORTED
1398}
1399
1400#[cfg(not(feature = "nat-traversal"))]
1401#[unsafe(no_mangle)]
1402pub unsafe extern "C" fn net_mesh_reclassify_nat(_handle: *mut MeshNodeHandle) -> c_int {
1403 NET_ERR_TRAVERSAL_UNSUPPORTED
1404}
1405
1406#[cfg(not(feature = "nat-traversal"))]
1407#[unsafe(no_mangle)]
1408pub unsafe extern "C" fn net_mesh_traversal_stats(
1409 _handle: *mut MeshNodeHandle,
1410 _out_punches_attempted: *mut u64,
1411 _out_punches_succeeded: *mut u64,
1412 _out_relay_fallbacks: *mut u64,
1413) -> c_int {
1414 NET_ERR_TRAVERSAL_UNSUPPORTED
1415}
1416
1417#[cfg(not(feature = "nat-traversal"))]
1418#[unsafe(no_mangle)]
1419pub unsafe extern "C" fn net_mesh_connect_direct(
1420 _handle: *mut MeshNodeHandle,
1421 _peer_node_id: u64,
1422 _peer_pubkey_hex: *const c_char,
1423 _coordinator: u64,
1424) -> c_int {
1425 NET_ERR_TRAVERSAL_UNSUPPORTED
1426}
1427
1428#[cfg(not(feature = "nat-traversal"))]
1429#[unsafe(no_mangle)]
1430pub unsafe extern "C" fn net_mesh_connect_direct_auto(
1431 _handle: *mut MeshNodeHandle,
1432 _peer_node_id: u64,
1433 _peer_pubkey_hex: *const c_char,
1434) -> c_int {
1435 NET_ERR_TRAVERSAL_UNSUPPORTED
1436}
1437
1438#[cfg(not(feature = "nat-traversal"))]
1439#[unsafe(no_mangle)]
1440pub unsafe extern "C" fn net_mesh_traversal_stats_v2(
1441 _handle: *mut MeshNodeHandle,
1442 _out: *mut NetTraversalStatsV2,
1443) -> c_int {
1444 NET_ERR_TRAVERSAL_UNSUPPORTED
1445}
1446
1447#[cfg(not(feature = "nat-traversal"))]
1448#[unsafe(no_mangle)]
1449pub unsafe extern "C" fn net_mesh_set_reflex_override(
1450 _handle: *mut MeshNodeHandle,
1451 _external: *const c_char,
1452) -> c_int {
1453 NET_ERR_TRAVERSAL_UNSUPPORTED
1454}
1455
1456#[cfg(not(feature = "nat-traversal"))]
1457#[unsafe(no_mangle)]
1458pub unsafe extern "C" fn net_mesh_clear_reflex_override(_handle: *mut MeshNodeHandle) -> c_int {
1459 NET_ERR_TRAVERSAL_UNSUPPORTED
1460}
1461
1462#[derive(Deserialize, Default)]
1467struct StreamOpenConfig {
1468 reliability: Option<String>,
1470 window_bytes: Option<u32>,
1473 fairness_weight: Option<u8>,
1474}
1475
1476pub struct MeshStreamHandle {
1491 stream: ManuallyDrop<CoreStream>,
1492 _node: ManuallyDrop<Arc<MeshNode>>,
1495 guard: HandleGuard,
1496}
1497
1498#[unsafe(no_mangle)]
1499pub unsafe extern "C" fn net_mesh_open_stream(
1500 handle: *mut MeshNodeHandle,
1501 peer_node_id: u64,
1502 stream_id: u64,
1503 config_json: *const c_char,
1504 out_stream: *mut *mut MeshStreamHandle,
1505) -> c_int {
1506 if handle.is_null() || out_stream.is_null() {
1507 return NetError::NullPointer.into();
1508 }
1509 let h = unsafe { &*handle };
1510 let _op = match h.guard.try_enter() {
1511 Some(op) => op,
1512 None => return NetError::ShuttingDown.into(),
1513 };
1514 let cfg_json: StreamOpenConfig = if config_json.is_null() {
1515 StreamOpenConfig::default()
1516 } else {
1517 let Some(s) = (unsafe { c_str_to_string(config_json) }) else {
1518 return NetError::InvalidUtf8.into();
1519 };
1520 match serde_json::from_str(&s) {
1521 Ok(v) => v,
1522 Err(_) => return NetError::InvalidJson.into(),
1523 }
1524 };
1525 let reliability = match cfg_json.reliability.as_deref() {
1526 None | Some("fire_and_forget") => Reliability::FireAndForget,
1527 Some("reliable") => Reliability::Reliable,
1528 Some(_) => return NET_ERR_MESH_TRANSPORT,
1529 };
1530 let window = cfg_json.window_bytes.unwrap_or(DEFAULT_STREAM_WINDOW_BYTES);
1531 let weight = cfg_json.fairness_weight.unwrap_or(1);
1532 let cfg = StreamConfig::new()
1533 .with_reliability(reliability)
1534 .with_window_bytes(window)
1535 .with_fairness_weight(weight);
1536 match h.inner.open_stream(peer_node_id, stream_id, cfg) {
1537 Ok(stream) => {
1538 let node_clone: Arc<MeshNode> = Arc::clone(&h.inner);
1539 let sh = Box::new(MeshStreamHandle {
1540 stream: ManuallyDrop::new(stream),
1541 _node: ManuallyDrop::new(node_clone),
1542 guard: HandleGuard::new(),
1543 });
1544 unsafe {
1545 *out_stream = Box::into_raw(sh);
1546 }
1547 0
1548 }
1549 Err(e) => adapter_err_to_code(&e),
1550 }
1551}
1552
1553#[unsafe(no_mangle)]
1554pub unsafe extern "C" fn net_mesh_stream_free(handle: *mut MeshStreamHandle) {
1555 if handle.is_null() {
1556 return;
1557 }
1558 let h: &MeshStreamHandle = unsafe { &*handle };
1560 if h.guard.begin_free(FFI_HANDLE_FREE_DEADLINE) {
1561 unsafe {
1563 let _stream = ManuallyDrop::take(&mut (*handle).stream);
1567 let node = ManuallyDrop::take(&mut (*handle)._node);
1568 drop(node);
1569 }
1570 } else {
1571 tracing::warn!(
1572 "net_mesh_stream_free: in-flight ops did not drain within deadline; \
1573 leaking inner to avoid use-after-free"
1574 );
1575 }
1576}
1577
1578unsafe fn collect_payloads(
1588 payloads: *const *const u8,
1589 lens: *const usize,
1590 count: usize,
1591) -> Option<Vec<Bytes>> {
1592 let mut out = Vec::with_capacity(count);
1593 for i in 0..count {
1594 let ptr = *payloads.add(i);
1595 let len = *lens.add(i);
1596 if ptr.is_null() {
1597 if len == 0 {
1598 out.push(Bytes::new());
1599 continue;
1600 }
1601 return None;
1602 }
1603 if len > isize::MAX as usize {
1607 return None;
1608 }
1609 let slice = std::slice::from_raw_parts(ptr, len);
1610 out.push(Bytes::copy_from_slice(slice));
1611 }
1612 Some(out)
1613}
1614
1615#[inline]
1623fn handles_match(sh: &MeshStreamHandle, nh: &MeshNodeHandle) -> bool {
1624 Arc::ptr_eq(&sh._node, &nh.inner)
1625}
1626
1627#[unsafe(no_mangle)]
1628pub unsafe extern "C" fn net_mesh_send(
1629 handle: *mut MeshStreamHandle,
1630 payloads: *const *const u8,
1631 lens: *const usize,
1632 count: usize,
1633 node_handle: *mut MeshNodeHandle,
1634) -> c_int {
1635 if handle.is_null() || node_handle.is_null() {
1636 return NetError::NullPointer.into();
1637 }
1638 if count > 0 && (payloads.is_null() || lens.is_null()) {
1639 return NetError::NullPointer.into();
1640 }
1641 let sh = unsafe { &*handle };
1642 let nh = unsafe { &*node_handle };
1643 let _sh_op = match sh.guard.try_enter() {
1646 Some(op) => op,
1647 None => return NetError::ShuttingDown.into(),
1648 };
1649 let _nh_op = match nh.guard.try_enter() {
1650 Some(op) => op,
1651 None => return NetError::ShuttingDown.into(),
1652 };
1653 if !handles_match(sh, nh) {
1654 return NetError::MismatchedHandles.into();
1655 }
1656 let payloads = match unsafe { collect_payloads(payloads, lens, count) } {
1657 Some(v) => v,
1658 None => return NetError::NullPointer.into(),
1659 };
1660 let node = nh.inner.clone();
1661 let stream = sh.stream.clone();
1662 match block_on(async move { node.send_on_stream(&stream, &payloads).await }) {
1663 Ok(()) => 0,
1664 Err(e) => stream_err_to_code(&e),
1665 }
1666}
1667
1668#[unsafe(no_mangle)]
1669pub unsafe extern "C" fn net_mesh_send_with_retry(
1670 handle: *mut MeshStreamHandle,
1671 payloads: *const *const u8,
1672 lens: *const usize,
1673 count: usize,
1674 max_retries: u32,
1675 node_handle: *mut MeshNodeHandle,
1676) -> c_int {
1677 if handle.is_null() || node_handle.is_null() {
1678 return NetError::NullPointer.into();
1679 }
1680 if count > 0 && (payloads.is_null() || lens.is_null()) {
1681 return NetError::NullPointer.into();
1682 }
1683 let sh = unsafe { &*handle };
1684 let nh = unsafe { &*node_handle };
1685 let _sh_op = match sh.guard.try_enter() {
1688 Some(op) => op,
1689 None => return NetError::ShuttingDown.into(),
1690 };
1691 let _nh_op = match nh.guard.try_enter() {
1692 Some(op) => op,
1693 None => return NetError::ShuttingDown.into(),
1694 };
1695 if !handles_match(sh, nh) {
1696 return NetError::MismatchedHandles.into();
1697 }
1698 let payloads = match unsafe { collect_payloads(payloads, lens, count) } {
1699 Some(v) => v,
1700 None => return NetError::NullPointer.into(),
1701 };
1702 let node = nh.inner.clone();
1703 let stream = sh.stream.clone();
1704 match block_on(async move {
1705 node.send_with_retry(&stream, &payloads, max_retries as usize)
1706 .await
1707 }) {
1708 Ok(()) => 0,
1709 Err(e) => stream_err_to_code(&e),
1710 }
1711}
1712
1713#[unsafe(no_mangle)]
1714pub unsafe extern "C" fn net_mesh_send_blocking(
1715 handle: *mut MeshStreamHandle,
1716 payloads: *const *const u8,
1717 lens: *const usize,
1718 count: usize,
1719 node_handle: *mut MeshNodeHandle,
1720) -> c_int {
1721 if handle.is_null() || node_handle.is_null() {
1722 return NetError::NullPointer.into();
1723 }
1724 if count > 0 && (payloads.is_null() || lens.is_null()) {
1725 return NetError::NullPointer.into();
1726 }
1727 let sh = unsafe { &*handle };
1728 let nh = unsafe { &*node_handle };
1729 let _sh_op = match sh.guard.try_enter() {
1732 Some(op) => op,
1733 None => return NetError::ShuttingDown.into(),
1734 };
1735 let _nh_op = match nh.guard.try_enter() {
1736 Some(op) => op,
1737 None => return NetError::ShuttingDown.into(),
1738 };
1739 if !handles_match(sh, nh) {
1740 return NetError::MismatchedHandles.into();
1741 }
1742 let payloads = match unsafe { collect_payloads(payloads, lens, count) } {
1743 Some(v) => v,
1744 None => return NetError::NullPointer.into(),
1745 };
1746 let node = nh.inner.clone();
1747 let stream = sh.stream.clone();
1748 match block_on(async move { node.send_blocking(&stream, &payloads).await }) {
1749 Ok(()) => 0,
1750 Err(e) => stream_err_to_code(&e),
1751 }
1752}
1753
1754#[derive(Serialize)]
1755struct StreamStatsJson {
1756 tx_seq: u64,
1757 rx_seq: u64,
1758 inbound_pending: u64,
1759 last_activity_ns: u64,
1760 active: bool,
1761 backpressure_events: u64,
1762 tx_credit_remaining: u32,
1763 tx_window: u32,
1764 credit_grants_received: u64,
1765 credit_grants_sent: u64,
1766}
1767
1768#[unsafe(no_mangle)]
1769pub unsafe extern "C" fn net_mesh_stream_stats(
1770 node_handle: *mut MeshNodeHandle,
1771 peer_node_id: u64,
1772 stream_id: u64,
1773 out_json: *mut *mut c_char,
1774 out_len: *mut usize,
1775) -> c_int {
1776 if node_handle.is_null() || out_json.is_null() || out_len.is_null() {
1777 return NetError::NullPointer.into();
1778 }
1779 let h = unsafe { &*node_handle };
1780 let _op = match h.guard.try_enter() {
1781 Some(op) => op,
1782 None => return NetError::ShuttingDown.into(),
1783 };
1784 match h.inner.stream_stats(peer_node_id, stream_id) {
1785 Some(s) => {
1786 let js = StreamStatsJson {
1787 tx_seq: s.tx_seq,
1788 rx_seq: s.rx_seq,
1789 inbound_pending: s.inbound_pending,
1790 last_activity_ns: s.last_activity_ns,
1791 active: s.active,
1792 backpressure_events: s.backpressure_events,
1793 tx_credit_remaining: s.tx_credit_remaining,
1794 tx_window: s.tx_window,
1795 credit_grants_received: s.credit_grants_received,
1796 credit_grants_sent: s.credit_grants_sent,
1797 };
1798 write_json_out(&js, out_json, out_len)
1799 }
1800 None => {
1801 write_string_out("null".to_string(), out_json, out_len)
1804 }
1805 }
1806}
1807
1808#[derive(Serialize)]
1813struct RecvEventJson {
1814 id: String,
1815 payload_b64: String,
1817 insertion_ts: u64,
1818 shard_id: u16,
1819}
1820
1821#[unsafe(no_mangle)]
1822pub unsafe extern "C" fn net_mesh_recv_shard(
1823 handle: *mut MeshNodeHandle,
1824 shard_id: u16,
1825 limit: u32,
1826 out_json: *mut *mut c_char,
1827 out_len: *mut usize,
1828) -> c_int {
1829 if handle.is_null() || out_json.is_null() || out_len.is_null() {
1830 return NetError::NullPointer.into();
1831 }
1832 let h = unsafe { &*handle };
1833 let _op = match h.guard.try_enter() {
1834 Some(op) => op,
1835 None => return NetError::ShuttingDown.into(),
1836 };
1837 let node = h.inner.clone();
1838 let result = block_on(async move { node.poll_shard(shard_id, None, limit as usize).await });
1839 let result = match result {
1840 Ok(r) => r,
1841 Err(e) => return adapter_err_to_code(&e),
1842 };
1843 let events: Vec<RecvEventJson> = result
1844 .events
1845 .into_iter()
1846 .map(|e| RecvEventJson {
1847 id: e.id,
1848 payload_b64: encode_b64(&e.raw),
1849 insertion_ts: e.insertion_ts,
1850 shard_id: e.shard_id,
1851 })
1852 .collect();
1853 write_json_out(&events, out_json, out_len)
1854}
1855
1856fn encode_b64(bytes: &[u8]) -> String {
1857 const ALPH: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
1860 let mut s = String::with_capacity(bytes.len().div_ceil(3) * 4);
1861 let mut i = 0;
1862 while i + 3 <= bytes.len() {
1863 let chunk = &bytes[i..i + 3];
1864 s.push(ALPH[(chunk[0] >> 2) as usize] as char);
1865 s.push(ALPH[(((chunk[0] & 0b11) << 4) | (chunk[1] >> 4)) as usize] as char);
1866 s.push(ALPH[(((chunk[1] & 0b1111) << 2) | (chunk[2] >> 6)) as usize] as char);
1867 s.push(ALPH[(chunk[2] & 0b111111) as usize] as char);
1868 i += 3;
1869 }
1870 let rem = bytes.len() - i;
1871 if rem == 1 {
1872 let b = bytes[i];
1873 s.push(ALPH[(b >> 2) as usize] as char);
1874 s.push(ALPH[((b & 0b11) << 4) as usize] as char);
1875 s.push('=');
1876 s.push('=');
1877 } else if rem == 2 {
1878 let b0 = bytes[i];
1879 let b1 = bytes[i + 1];
1880 s.push(ALPH[(b0 >> 2) as usize] as char);
1881 s.push(ALPH[(((b0 & 0b11) << 4) | (b1 >> 4)) as usize] as char);
1882 s.push(ALPH[((b1 & 0b1111) << 2) as usize] as char);
1883 s.push('=');
1884 }
1885 s
1886}
1887
1888#[derive(Deserialize)]
1893struct ChannelConfigInput {
1894 name: String,
1895 visibility: Option<String>,
1896 reliable: Option<bool>,
1897 require_token: Option<bool>,
1898 token_roots: Option<Vec<String>>,
1904 priority: Option<u8>,
1905 max_rate_pps: Option<u32>,
1906 publish_caps: Option<CapabilityFilterJson>,
1910 subscribe_caps: Option<CapabilityFilterJson>,
1914}
1915
1916fn parse_visibility(s: &str) -> Option<InnerVisibility> {
1917 match s {
1918 "subnet-local" => Some(InnerVisibility::SubnetLocal),
1919 "parent-visible" => Some(InnerVisibility::ParentVisible),
1920 "exported" => Some(InnerVisibility::Exported),
1921 "global" => Some(InnerVisibility::Global),
1922 _ => None,
1923 }
1924}
1925
1926#[unsafe(no_mangle)]
1927pub unsafe extern "C" fn net_mesh_register_channel(
1928 handle: *mut MeshNodeHandle,
1929 config_json: *const c_char,
1930) -> c_int {
1931 if handle.is_null() || config_json.is_null() {
1932 return NetError::NullPointer.into();
1933 }
1934 let h = unsafe { &*handle };
1935 let _op = match h.guard.try_enter() {
1936 Some(op) => op,
1937 None => return NetError::ShuttingDown.into(),
1938 };
1939 let Some(s) = (unsafe { c_str_to_string(config_json) }) else {
1940 return NetError::InvalidUtf8.into();
1941 };
1942 let input: ChannelConfigInput = match serde_json::from_str(&s) {
1943 Ok(v) => v,
1944 Err(_) => return NetError::InvalidJson.into(),
1945 };
1946 let name = match InnerChannelName::new(&input.name) {
1947 Ok(n) => n,
1948 Err(_) => return NET_ERR_CHANNEL,
1949 };
1950 let mut cfg = InnerChannelConfig::new(ChannelId::new(name));
1951 if let Some(v) = input.visibility {
1952 let Some(vis) = parse_visibility(&v) else {
1953 return NET_ERR_CHANNEL;
1954 };
1955 cfg = cfg.with_visibility(vis);
1956 }
1957 if let Some(r) = input.reliable {
1958 cfg = cfg.with_reliable(r);
1959 }
1960 if let Some(t) = input.require_token {
1961 cfg = cfg.with_require_token(t);
1962 }
1963 if let Some(roots) = input.token_roots {
1964 let mut parsed = Vec::with_capacity(roots.len());
1965 for hex_id in roots {
1966 let bytes = match hex::decode(&hex_id) {
1967 Ok(b) => b,
1968 Err(_) => return NET_ERR_CHANNEL,
1969 };
1970 let Ok(arr) = <[u8; 32]>::try_from(bytes.as_slice()) else {
1971 return NET_ERR_CHANNEL;
1972 };
1973 parsed.push(EntityId::from_bytes(arr));
1974 }
1975 cfg = cfg.with_token_roots(parsed);
1976 }
1977 if let Some(p) = input.priority {
1978 cfg = cfg.with_priority(p);
1979 }
1980 if let Some(pps) = input.max_rate_pps {
1981 cfg = cfg.with_rate_limit(pps);
1982 }
1983 if let Some(filter_json) = input.publish_caps {
1984 cfg = cfg.with_publish_caps(capability_filter_from_json(filter_json));
1985 }
1986 if let Some(filter_json) = input.subscribe_caps {
1987 cfg = cfg.with_subscribe_caps(capability_filter_from_json(filter_json));
1988 }
1989 h.channel_configs.insert(cfg);
1990 0
1991}
1992
1993#[unsafe(no_mangle)]
1994pub unsafe extern "C" fn net_mesh_subscribe_channel(
1995 handle: *mut MeshNodeHandle,
1996 publisher_node_id: u64,
1997 channel: *const c_char,
1998) -> c_int {
1999 subscribe_or_unsubscribe(handle, publisher_node_id, channel, true)
2000}
2001
2002#[unsafe(no_mangle)]
2003pub unsafe extern "C" fn net_mesh_unsubscribe_channel(
2004 handle: *mut MeshNodeHandle,
2005 publisher_node_id: u64,
2006 channel: *const c_char,
2007) -> c_int {
2008 subscribe_or_unsubscribe(handle, publisher_node_id, channel, false)
2009}
2010
2011#[unsafe(no_mangle)]
2018pub unsafe extern "C" fn net_mesh_subscribe_channel_with_token(
2019 handle: *mut MeshNodeHandle,
2020 publisher_node_id: u64,
2021 channel: *const c_char,
2022 token: *const u8,
2023 token_len: usize,
2024) -> c_int {
2025 if handle.is_null() || channel.is_null() || token.is_null() {
2026 return NetError::NullPointer.into();
2027 }
2028 let h = unsafe { &*handle };
2029 let _op = match h.guard.try_enter() {
2030 Some(op) => op,
2031 None => return NetError::ShuttingDown.into(),
2032 };
2033 let Some(s) = (unsafe { c_str_to_string(channel) }) else {
2034 return NetError::InvalidUtf8.into();
2035 };
2036 let name = match InnerChannelName::new(&s) {
2037 Ok(n) => n,
2038 Err(_) => return NET_ERR_CHANNEL,
2039 };
2040 if token_len > isize::MAX as usize {
2042 return NetError::InvalidJson.into();
2043 }
2044 let slice = unsafe { std::slice::from_raw_parts(token, token_len) };
2045 let parsed = match PermissionToken::from_bytes(slice) {
2046 Ok(t) => t,
2047 Err(e) => return token_err_to_code(&e),
2048 };
2049 let node = h.inner.clone();
2050 match block_on(async move {
2051 node.subscribe_channel_with_token(publisher_node_id, name, parsed)
2052 .await
2053 }) {
2054 Ok(()) => 0,
2055 Err(e) => adapter_err_to_channel_code(&e),
2056 }
2057}
2058
2059fn subscribe_or_unsubscribe(
2060 handle: *mut MeshNodeHandle,
2061 publisher_node_id: u64,
2062 channel: *const c_char,
2063 subscribe: bool,
2064) -> c_int {
2065 if handle.is_null() || channel.is_null() {
2066 return NetError::NullPointer.into();
2067 }
2068 let h = unsafe { &*handle };
2069 let _op = match h.guard.try_enter() {
2070 Some(op) => op,
2071 None => return NetError::ShuttingDown.into(),
2072 };
2073 let Some(s) = (unsafe { c_str_to_string(channel) }) else {
2074 return NetError::InvalidUtf8.into();
2075 };
2076 let name = match InnerChannelName::new(&s) {
2077 Ok(n) => n,
2078 Err(_) => return NET_ERR_CHANNEL,
2079 };
2080 let node = h.inner.clone();
2081 let outcome = if subscribe {
2082 block_on(async move { node.subscribe_channel(publisher_node_id, name).await })
2083 } else {
2084 block_on(async move { node.unsubscribe_channel(publisher_node_id, name).await })
2085 };
2086 match outcome {
2087 Ok(()) => 0,
2088 Err(e) => adapter_err_to_channel_code(&e),
2089 }
2090}
2091
2092fn adapter_err_to_channel_code(err: &AdapterError) -> c_int {
2093 if let AdapterError::Connection(msg) = err {
2094 let prefix = "membership request rejected: ";
2095 if let Some(tail) = msg.strip_prefix(prefix) {
2096 if tail.trim() == "Some(Unauthorized)" {
2097 return NET_ERR_CHANNEL_AUTH;
2098 }
2099 }
2100 }
2101 NET_ERR_CHANNEL
2102}
2103
2104#[derive(Deserialize, Default)]
2105struct PublishConfigInput {
2106 reliability: Option<String>,
2107 on_failure: Option<String>,
2108 max_inflight: Option<u32>,
2109}
2110
2111#[derive(Serialize)]
2112struct PublishReportJson {
2113 attempted: u32,
2114 delivered: u32,
2115 errors: Vec<PublishFailureJson>,
2116}
2117
2118#[derive(Serialize)]
2119struct PublishFailureJson {
2120 node_id: u64,
2121 message: String,
2122}
2123
2124fn to_publish_report_json(r: InnerPublishReport) -> PublishReportJson {
2125 PublishReportJson {
2126 attempted: r.attempted as u32,
2127 delivered: r.delivered as u32,
2128 errors: r
2129 .errors
2130 .into_iter()
2131 .map(|(id, e)| PublishFailureJson {
2132 node_id: id,
2133 message: format!("{}", e),
2134 })
2135 .collect(),
2136 }
2137}
2138
2139#[unsafe(no_mangle)]
2140pub unsafe extern "C" fn net_mesh_publish(
2141 handle: *mut MeshNodeHandle,
2142 channel: *const c_char,
2143 payload: *const u8,
2144 len: usize,
2145 config_json: *const c_char,
2146 out_json: *mut *mut c_char,
2147 out_len: *mut usize,
2148) -> c_int {
2149 if handle.is_null() || channel.is_null() || out_json.is_null() || out_len.is_null() {
2150 return NetError::NullPointer.into();
2151 }
2152 let h = unsafe { &*handle };
2153 let _op = match h.guard.try_enter() {
2154 Some(op) => op,
2155 None => return NetError::ShuttingDown.into(),
2156 };
2157 let Some(ch) = (unsafe { c_str_to_string(channel) }) else {
2158 return NetError::InvalidUtf8.into();
2159 };
2160 let name = match InnerChannelName::new(&ch) {
2161 Ok(n) => n,
2162 Err(_) => return NET_ERR_CHANNEL,
2163 };
2164 let cfg_in: PublishConfigInput = if config_json.is_null() {
2165 PublishConfigInput::default()
2166 } else {
2167 let Some(s) = (unsafe { c_str_to_string(config_json) }) else {
2168 return NetError::InvalidUtf8.into();
2169 };
2170 match serde_json::from_str(&s) {
2171 Ok(v) => v,
2172 Err(_) => return NetError::InvalidJson.into(),
2173 }
2174 };
2175 let reliability = match cfg_in.reliability.as_deref() {
2176 None | Some("fire_and_forget") => Reliability::FireAndForget,
2177 Some("reliable") => Reliability::Reliable,
2178 Some(_) => return NET_ERR_CHANNEL,
2179 };
2180 let on_failure = match cfg_in.on_failure.as_deref() {
2181 None | Some("best_effort") => InnerOnFailure::BestEffort,
2182 Some("fail_fast") => InnerOnFailure::FailFast,
2183 Some("collect") => InnerOnFailure::Collect,
2184 Some(_) => return NET_ERR_CHANNEL,
2185 };
2186 let max_inflight = cfg_in.max_inflight.unwrap_or(32) as usize;
2187 let publish_cfg = InnerPublishConfig {
2188 reliability,
2189 on_failure,
2190 max_inflight,
2191 };
2192 let publisher = ChannelPublisher::new(name, publish_cfg);
2193
2194 let bytes = if len == 0 {
2196 Bytes::new()
2197 } else if payload.is_null() {
2198 return NetError::NullPointer.into();
2199 } else if len > isize::MAX as usize {
2200 return NetError::InvalidJson.into();
2202 } else {
2203 Bytes::copy_from_slice(unsafe { std::slice::from_raw_parts(payload, len) })
2204 };
2205
2206 let node = h.inner.clone();
2207 match block_on(async move { node.publish(&publisher, bytes).await }) {
2208 Ok(report) => {
2209 let js = to_publish_report_json(report);
2210 write_json_out(&js, out_json, out_len)
2211 }
2212 Err(e) => adapter_err_to_channel_code(&e),
2213 }
2214}
2215
2216pub struct IdentityHandle {
2230 keypair: ManuallyDrop<Arc<EntityKeypair>>,
2231 cache: ManuallyDrop<Arc<TokenCache>>,
2232 guard: HandleGuard,
2233}
2234
2235fn alloc_bytes(src: &[u8], out_ptr: *mut *mut u8, out_len: *mut usize) -> c_int {
2249 if out_ptr.is_null() || out_len.is_null() {
2250 return NetError::NullPointer.into();
2251 }
2252 let len = src.len();
2253 if len == 0 {
2254 unsafe {
2255 *out_ptr = std::ptr::null_mut();
2256 *out_len = 0;
2257 }
2258 return 0;
2259 }
2260 let layout = match std::alloc::Layout::array::<u8>(len) {
2269 Ok(l) => l,
2270 Err(_) => return NET_ERR_IDENTITY,
2276 };
2277 let ptr = unsafe { std::alloc::alloc(layout) };
2278 if ptr.is_null() {
2279 std::alloc::handle_alloc_error(layout);
2280 }
2281 unsafe {
2282 std::ptr::copy_nonoverlapping(src.as_ptr(), ptr, len);
2283 *out_ptr = ptr;
2284 *out_len = len;
2285 }
2286 0
2287}
2288
2289#[unsafe(no_mangle)]
2304pub unsafe extern "C" fn net_free_bytes(ptr: *mut u8, len: usize) {
2305 if ptr.is_null() || len == 0 {
2306 return;
2307 }
2308 let layout = match std::alloc::Layout::array::<u8>(len) {
2314 Ok(l) => l,
2315 Err(_) => return,
2316 };
2317 unsafe {
2318 std::alloc::dealloc(ptr, layout);
2319 }
2320}
2321
2322fn entity_id_from_bytes(bytes: *const u8, len: usize) -> Option<EntityId> {
2323 if bytes.is_null() || len != 32 {
2324 return None;
2325 }
2326 let slice = unsafe { std::slice::from_raw_parts(bytes, 32) };
2327 let mut arr = [0u8; 32];
2328 arr.copy_from_slice(slice);
2329 Some(EntityId::from_bytes(arr))
2330}
2331
2332fn parse_scope_list(raw: &str) -> Option<TokenScope> {
2333 let values: Vec<String> = serde_json::from_str(raw).ok()?;
2337 let mut acc = TokenScope::NONE;
2338 for s in &values {
2339 acc = acc.union(match s.as_str() {
2340 "publish" => TokenScope::PUBLISH,
2341 "subscribe" => TokenScope::SUBSCRIBE,
2342 "admin" => TokenScope::ADMIN,
2343 "delegate" => TokenScope::DELEGATE,
2344 _ => return None,
2345 });
2346 }
2347 Some(acc)
2348}
2349
2350fn scope_to_strings(scope: TokenScope) -> Vec<&'static str> {
2351 let mut out = Vec::new();
2352 if scope.contains(TokenScope::PUBLISH) {
2353 out.push("publish");
2354 }
2355 if scope.contains(TokenScope::SUBSCRIBE) {
2356 out.push("subscribe");
2357 }
2358 if scope.contains(TokenScope::ADMIN) {
2359 out.push("admin");
2360 }
2361 if scope.contains(TokenScope::DELEGATE) {
2362 out.push("delegate");
2363 }
2364 out
2365}
2366
2367fn channel_name_to_hash(channel: &str) -> Option<ChannelHash> {
2368 InnerChannelName::new(channel).ok().map(|n| n.hash())
2369}
2370
2371#[unsafe(no_mangle)]
2374pub unsafe extern "C" fn net_identity_generate(out_handle: *mut *mut IdentityHandle) -> c_int {
2375 if out_handle.is_null() {
2376 return NetError::NullPointer.into();
2377 }
2378 let handle = Box::new(IdentityHandle {
2379 keypair: ManuallyDrop::new(Arc::new(EntityKeypair::generate())),
2380 cache: ManuallyDrop::new(Arc::new(TokenCache::new())),
2381 guard: HandleGuard::new(),
2382 });
2383 unsafe {
2384 *out_handle = Box::into_raw(handle);
2385 }
2386 0
2387}
2388
2389#[unsafe(no_mangle)]
2393pub unsafe extern "C" fn net_identity_from_seed(
2394 seed: *const u8,
2395 seed_len: usize,
2396 out_handle: *mut *mut IdentityHandle,
2397) -> c_int {
2398 if seed.is_null() || out_handle.is_null() {
2399 return NetError::NullPointer.into();
2400 }
2401 if seed_len != 32 {
2402 return NET_ERR_IDENTITY;
2403 }
2404 let mut arr = [0u8; 32];
2405 arr.copy_from_slice(unsafe { std::slice::from_raw_parts(seed, 32) });
2406 let handle = Box::new(IdentityHandle {
2407 keypair: ManuallyDrop::new(Arc::new(EntityKeypair::from_bytes(arr))),
2408 cache: ManuallyDrop::new(Arc::new(TokenCache::new())),
2409 guard: HandleGuard::new(),
2410 });
2411 unsafe {
2412 *out_handle = Box::into_raw(handle);
2413 }
2414 0
2415}
2416
2417#[unsafe(no_mangle)]
2418pub unsafe extern "C" fn net_identity_free(handle: *mut IdentityHandle) {
2419 if handle.is_null() {
2420 return;
2421 }
2422 let h: &IdentityHandle = unsafe { &*handle };
2424 if h.guard.begin_free(FFI_HANDLE_FREE_DEADLINE) {
2425 unsafe {
2427 let mh = &mut *handle;
2428 let kp = ManuallyDrop::take(&mut mh.keypair);
2429 let cache = ManuallyDrop::take(&mut mh.cache);
2430 drop(kp);
2431 drop(cache);
2432 }
2433 } else {
2434 tracing::warn!(
2435 "net_identity_free: in-flight ops did not drain within deadline; \
2436 leaking inner to avoid use-after-free"
2437 );
2438 }
2439}
2440
2441#[unsafe(no_mangle)]
2444pub unsafe extern "C" fn net_identity_to_seed(handle: *mut IdentityHandle, out: *mut u8) -> c_int {
2445 if handle.is_null() || out.is_null() {
2446 return NetError::NullPointer.into();
2447 }
2448 let h = unsafe { &*handle };
2449 let _op = match h.guard.try_enter() {
2450 Some(op) => op,
2451 None => return NetError::ShuttingDown.into(),
2452 };
2453 let seed = h.keypair.secret_bytes();
2454 unsafe {
2455 std::ptr::copy_nonoverlapping(seed.as_ptr(), out, 32);
2456 }
2457 0
2458}
2459
2460#[unsafe(no_mangle)]
2462pub unsafe extern "C" fn net_identity_entity_id(
2463 handle: *mut IdentityHandle,
2464 out: *mut u8,
2465) -> c_int {
2466 if handle.is_null() || out.is_null() {
2467 return NetError::NullPointer.into();
2468 }
2469 let h = unsafe { &*handle };
2470 let _op = match h.guard.try_enter() {
2471 Some(op) => op,
2472 None => return NetError::ShuttingDown.into(),
2473 };
2474 let id = h.keypair.entity_id().as_bytes();
2475 unsafe {
2476 std::ptr::copy_nonoverlapping(id.as_ptr(), out, 32);
2477 }
2478 0
2479}
2480
2481#[unsafe(no_mangle)]
2482pub unsafe extern "C" fn net_identity_node_id(handle: *mut IdentityHandle) -> u64 {
2483 if handle.is_null() {
2484 return 0;
2485 }
2486 let h = unsafe { &*handle };
2487 let _op = match h.guard.try_enter() {
2489 Some(op) => op,
2490 None => return 0,
2491 };
2492 h.keypair.node_id()
2493}
2494
2495#[unsafe(no_mangle)]
2496pub unsafe extern "C" fn net_identity_origin_hash(handle: *mut IdentityHandle) -> u64 {
2497 if handle.is_null() {
2498 return 0;
2499 }
2500 let h = unsafe { &*handle };
2501 let _op = match h.guard.try_enter() {
2503 Some(op) => op,
2504 None => return 0,
2505 };
2506 h.keypair.origin_hash()
2507}
2508
2509#[unsafe(no_mangle)]
2512pub unsafe extern "C" fn net_identity_sign(
2513 handle: *mut IdentityHandle,
2514 msg: *const u8,
2515 len: usize,
2516 out_sig: *mut u8,
2517) -> c_int {
2518 if handle.is_null() || out_sig.is_null() {
2519 return NetError::NullPointer.into();
2520 }
2521 if len > 0 && msg.is_null() {
2522 return NetError::NullPointer.into();
2523 }
2524 let h = unsafe { &*handle };
2525 let _op = match h.guard.try_enter() {
2526 Some(op) => op,
2527 None => return NetError::ShuttingDown.into(),
2528 };
2529 let slice = if len == 0 {
2530 &[][..]
2531 } else if len > isize::MAX as usize {
2532 return NetError::InvalidJson.into();
2534 } else {
2535 unsafe { std::slice::from_raw_parts(msg, len) }
2536 };
2537 let sig = h.keypair.sign(slice).to_bytes();
2538 unsafe {
2539 std::ptr::copy_nonoverlapping(sig.as_ptr(), out_sig, 64);
2540 }
2541 0
2542}
2543
2544#[unsafe(no_mangle)]
2547pub unsafe extern "C" fn net_identity_issue_token(
2548 signer: *mut IdentityHandle,
2549 subject: *const u8,
2550 subject_len: usize,
2551 scope_json: *const c_char,
2552 channel: *const c_char,
2553 ttl_seconds: u32,
2554 delegation_depth: u8,
2555 out_token: *mut *mut u8,
2556 out_token_len: *mut usize,
2557) -> c_int {
2558 if signer.is_null() || out_token.is_null() || out_token_len.is_null() {
2559 return NetError::NullPointer.into();
2560 }
2561 let Some(subject_id) = entity_id_from_bytes(subject, subject_len) else {
2562 return NET_ERR_IDENTITY;
2563 };
2564 let Some(scope_s) = (unsafe { c_str_to_string(scope_json) }) else {
2565 return NetError::InvalidUtf8.into();
2566 };
2567 let Some(scope) = parse_scope_list(&scope_s) else {
2568 return NET_ERR_IDENTITY;
2569 };
2570 let Some(channel_s) = (unsafe { c_str_to_string(channel) }) else {
2571 return NetError::InvalidUtf8.into();
2572 };
2573 let Some(channel_hash) = channel_name_to_hash(&channel_s) else {
2574 return NET_ERR_IDENTITY;
2575 };
2576 let h = unsafe { &*signer };
2577 let _op = match h.guard.try_enter() {
2581 Some(op) => op,
2582 None => return NetError::ShuttingDown.into(),
2583 };
2584 let token = match PermissionToken::try_issue(
2590 &h.keypair,
2591 subject_id,
2592 scope,
2593 channel_hash,
2594 u64::from(ttl_seconds),
2595 delegation_depth,
2596 ) {
2597 Ok(t) => t,
2598 Err(e) => return token_err_to_code(&e),
2599 };
2600 alloc_bytes(&token.to_bytes(), out_token, out_token_len)
2601}
2602
2603#[unsafe(no_mangle)]
2607pub unsafe extern "C" fn net_identity_install_token(
2608 handle: *mut IdentityHandle,
2609 token: *const u8,
2610 len: usize,
2611) -> c_int {
2612 if handle.is_null() || token.is_null() {
2613 return NetError::NullPointer.into();
2614 }
2615 if len > isize::MAX as usize {
2617 return NetError::InvalidJson.into();
2618 }
2619 let slice = unsafe { std::slice::from_raw_parts(token, len) };
2620 let parsed = match PermissionToken::from_bytes(slice) {
2621 Ok(t) => t,
2622 Err(e) => return token_err_to_code(&e),
2623 };
2624 let h = unsafe { &*handle };
2625 let _op = match h.guard.try_enter() {
2626 Some(op) => op,
2627 None => return NetError::ShuttingDown.into(),
2628 };
2629 match h.cache.insert(parsed) {
2630 Ok(()) => 0,
2631 Err(e) => token_err_to_code(&e),
2632 }
2633}
2634
2635#[unsafe(no_mangle)]
2639pub unsafe extern "C" fn net_identity_lookup_token(
2640 handle: *mut IdentityHandle,
2641 subject: *const u8,
2642 subject_len: usize,
2643 channel: *const c_char,
2644 out_token: *mut *mut u8,
2645 out_token_len: *mut usize,
2646) -> c_int {
2647 if handle.is_null() || out_token.is_null() || out_token_len.is_null() {
2648 return NetError::NullPointer.into();
2649 }
2650 let Some(subject_id) = entity_id_from_bytes(subject, subject_len) else {
2651 return NET_ERR_IDENTITY;
2652 };
2653 let Some(channel_s) = (unsafe { c_str_to_string(channel) }) else {
2654 return NetError::InvalidUtf8.into();
2655 };
2656 let Some(channel_hash) = channel_name_to_hash(&channel_s) else {
2657 return NET_ERR_IDENTITY;
2658 };
2659 let h = unsafe { &*handle };
2660 let _op = match h.guard.try_enter() {
2661 Some(op) => op,
2662 None => return NetError::ShuttingDown.into(),
2663 };
2664 match h.cache.get(&subject_id, channel_hash) {
2665 Some(token) => alloc_bytes(&token.to_bytes(), out_token, out_token_len),
2666 None => {
2667 unsafe {
2668 *out_token = std::ptr::null_mut();
2669 *out_token_len = 0;
2670 }
2671 0
2672 }
2673 }
2674}
2675
2676#[unsafe(no_mangle)]
2677pub unsafe extern "C" fn net_identity_token_cache_len(handle: *mut IdentityHandle) -> u32 {
2678 if handle.is_null() {
2679 return 0;
2680 }
2681 let h = unsafe { &*handle };
2682 let _op = match h.guard.try_enter() {
2684 Some(op) => op,
2685 None => return 0,
2686 };
2687 h.cache.len() as u32
2688}
2689
2690#[derive(Serialize)]
2695struct ParsedTokenJson {
2696 issuer_hex: String,
2697 subject_hex: String,
2698 scope: Vec<&'static str>,
2699 channel_hash: ChannelHash,
2700 not_before: u64,
2701 not_after: u64,
2702 delegation_depth: u8,
2703 nonce: u64,
2704 signature_hex: String,
2705}
2706
2707#[unsafe(no_mangle)]
2712pub unsafe extern "C" fn net_parse_token(
2713 token: *const u8,
2714 len: usize,
2715 out_json: *mut *mut c_char,
2716 out_len: *mut usize,
2717) -> c_int {
2718 if token.is_null() || out_json.is_null() || out_len.is_null() {
2719 return NetError::NullPointer.into();
2720 }
2721 if len > isize::MAX as usize {
2723 return NetError::InvalidJson.into();
2724 }
2725 let slice = unsafe { std::slice::from_raw_parts(token, len) };
2726 let parsed = match PermissionToken::from_bytes(slice) {
2727 Ok(t) => t,
2728 Err(e) => return token_err_to_code(&e),
2729 };
2730 let out = ParsedTokenJson {
2731 issuer_hex: hex::encode(parsed.issuer.as_bytes()),
2732 subject_hex: hex::encode(parsed.subject.as_bytes()),
2733 scope: scope_to_strings(parsed.scope),
2734 channel_hash: parsed.channel_hash,
2735 not_before: parsed.not_before,
2736 not_after: parsed.not_after,
2737 delegation_depth: parsed.delegation_depth,
2738 nonce: parsed.nonce,
2739 signature_hex: hex::encode(parsed.signature),
2740 };
2741 write_json_out(&out, out_json, out_len)
2742}
2743
2744#[unsafe(no_mangle)]
2748pub unsafe extern "C" fn net_verify_token(
2749 token: *const u8,
2750 len: usize,
2751 out_ok: *mut c_int,
2752) -> c_int {
2753 if token.is_null() || out_ok.is_null() {
2754 return NetError::NullPointer.into();
2755 }
2756 if len > isize::MAX as usize {
2758 return NetError::InvalidJson.into();
2759 }
2760 let slice = unsafe { std::slice::from_raw_parts(token, len) };
2761 let parsed = match PermissionToken::from_bytes(slice) {
2762 Ok(t) => t,
2763 Err(e) => return token_err_to_code(&e),
2764 };
2765 unsafe {
2766 *out_ok = if parsed.verify().is_ok() { 1 } else { 0 };
2767 }
2768 0
2769}
2770
2771#[unsafe(no_mangle)]
2776pub unsafe extern "C" fn net_token_is_expired(
2777 token: *const u8,
2778 len: usize,
2779 out_expired: *mut c_int,
2780) -> c_int {
2781 if token.is_null() || out_expired.is_null() {
2782 return NetError::NullPointer.into();
2783 }
2784 if len > isize::MAX as usize {
2786 return NetError::InvalidJson.into();
2787 }
2788 let slice = unsafe { std::slice::from_raw_parts(token, len) };
2789 let parsed = match PermissionToken::from_bytes(slice) {
2790 Ok(t) => t,
2791 Err(e) => return token_err_to_code(&e),
2792 };
2793 unsafe {
2794 *out_expired = if parsed.is_expired() { 1 } else { 0 };
2795 }
2796 0
2797}
2798
2799#[unsafe(no_mangle)]
2802pub unsafe extern "C" fn net_delegate_token(
2803 signer: *mut IdentityHandle,
2804 parent: *const u8,
2805 parent_len: usize,
2806 new_subject: *const u8,
2807 new_subject_len: usize,
2808 restricted_scope_json: *const c_char,
2809 out_token: *mut *mut u8,
2810 out_token_len: *mut usize,
2811) -> c_int {
2812 if signer.is_null()
2813 || parent.is_null()
2814 || new_subject.is_null()
2815 || restricted_scope_json.is_null()
2816 || out_token.is_null()
2817 || out_token_len.is_null()
2818 {
2819 return NetError::NullPointer.into();
2820 }
2821 if parent_len > isize::MAX as usize {
2823 return NetError::InvalidJson.into();
2824 }
2825 let parent_slice = unsafe { std::slice::from_raw_parts(parent, parent_len) };
2826 let parent_tok = match PermissionToken::from_bytes(parent_slice) {
2827 Ok(t) => t,
2828 Err(e) => return token_err_to_code(&e),
2829 };
2830 let Some(subject_id) = entity_id_from_bytes(new_subject, new_subject_len) else {
2831 return NET_ERR_IDENTITY;
2832 };
2833 let Some(scope_s) = (unsafe { c_str_to_string(restricted_scope_json) }) else {
2834 return NetError::InvalidUtf8.into();
2835 };
2836 let Some(scope) = parse_scope_list(&scope_s) else {
2837 return NET_ERR_IDENTITY;
2838 };
2839 let h = unsafe { &*signer };
2840 let _op = match h.guard.try_enter() {
2844 Some(op) => op,
2845 None => return NetError::ShuttingDown.into(),
2846 };
2847 match parent_tok.delegate(&h.keypair, subject_id, scope) {
2848 Ok(child) => alloc_bytes(&child.to_bytes(), out_token, out_token_len),
2849 Err(e) => token_err_to_code(&e),
2850 }
2851}
2852
2853#[unsafe(no_mangle)]
2858pub unsafe extern "C" fn net_channel_hash(channel: *const c_char, out_hash: *mut u64) -> c_int {
2859 if channel.is_null() || out_hash.is_null() {
2860 return NetError::NullPointer.into();
2861 }
2862 let Some(s) = (unsafe { c_str_to_string(channel) }) else {
2863 return NetError::InvalidUtf8.into();
2864 };
2865 let Some(hash) = channel_name_to_hash(&s) else {
2866 return NET_ERR_IDENTITY;
2867 };
2868 unsafe {
2869 *out_hash = hash;
2870 }
2871 0
2872}
2873
2874use crate::adapter::net::behavior::capability::{
2881 AcceleratorInfo, AcceleratorType, CapabilityFilter, CapabilitySet, GpuInfo, GpuVendor,
2882 HardwareCapabilities, Modality, ModelCapability, ResourceLimits, SoftwareCapabilities,
2883 ToolCapability, TAG_SCOPE_REGION_PREFIX, TAG_SCOPE_SUBNET_LOCAL, TAG_SCOPE_TENANT_PREFIX,
2884};
2885
2886fn parse_gpu_vendor_cap(s: &str) -> GpuVendor {
2889 match s.to_ascii_lowercase().as_str() {
2890 "nvidia" => GpuVendor::Nvidia,
2891 "amd" => GpuVendor::Amd,
2892 "intel" => GpuVendor::Intel,
2893 "apple" => GpuVendor::Apple,
2894 "qualcomm" => GpuVendor::Qualcomm,
2895 _ => GpuVendor::Unknown,
2896 }
2897}
2898
2899fn gpu_vendor_to_string_cap(v: GpuVendor) -> &'static str {
2900 match v {
2901 GpuVendor::Nvidia => "nvidia",
2902 GpuVendor::Amd => "amd",
2903 GpuVendor::Intel => "intel",
2904 GpuVendor::Apple => "apple",
2905 GpuVendor::Qualcomm => "qualcomm",
2906 GpuVendor::Unknown => "unknown",
2907 }
2908}
2909
2910fn parse_modality_cap(s: &str) -> Option<Modality> {
2911 match s.to_ascii_lowercase().as_str() {
2912 "text" => Some(Modality::Text),
2913 "image" => Some(Modality::Image),
2914 "audio" => Some(Modality::Audio),
2915 "video" => Some(Modality::Video),
2916 "code" => Some(Modality::Code),
2917 "embedding" => Some(Modality::Embedding),
2918 "tool-use" | "tool_use" | "tooluse" => Some(Modality::ToolUse),
2919 _ => None,
2928 }
2929}
2930
2931fn parse_accelerator_type_cap(s: &str) -> AcceleratorType {
2932 match s.to_ascii_lowercase().as_str() {
2933 "tpu" => AcceleratorType::Tpu,
2934 "npu" => AcceleratorType::Npu,
2935 "fpga" => AcceleratorType::Fpga,
2936 "asic" => AcceleratorType::Asic,
2937 "dsp" => AcceleratorType::Dsp,
2938 _ => AcceleratorType::Unknown,
2939 }
2940}
2941
2942#[derive(Deserialize, Default)]
2945struct CapabilitySetJson {
2946 #[serde(default)]
2947 hardware: Option<HardwareJson>,
2948 #[serde(default)]
2949 software: Option<SoftwareJson>,
2950 #[serde(default)]
2951 models: Vec<ModelJson>,
2952 #[serde(default)]
2953 tools: Vec<ToolJson>,
2954 #[serde(default)]
2955 tags: Vec<String>,
2956 #[serde(default)]
2957 limits: Option<LimitsJson>,
2958}
2959
2960#[derive(Deserialize, Default)]
2961struct HardwareJson {
2962 cpu_cores: Option<u32>,
2963 cpu_threads: Option<u32>,
2964 memory_gb: Option<u32>,
2965 gpu: Option<GpuJson>,
2966 #[serde(default)]
2967 additional_gpus: Vec<GpuJson>,
2968 storage_gb: Option<u64>,
2969 network_gbps: Option<u32>,
2970 #[serde(default)]
2971 accelerators: Vec<AcceleratorJson>,
2972}
2973
2974#[derive(Deserialize)]
2975struct GpuJson {
2976 vendor: Option<String>,
2977 #[serde(default)]
2978 model: String,
2979 #[serde(default)]
2980 vram_gb: u32,
2981 compute_units: Option<u32>,
2982 tensor_cores: Option<u32>,
2983 fp16_tflops_x10: Option<u32>,
2984}
2985
2986#[derive(Deserialize)]
2987struct AcceleratorJson {
2988 #[serde(default)]
2989 kind: String,
2990 #[serde(default)]
2991 model: String,
2992 memory_gb: Option<u32>,
2993 tops_x10: Option<u32>,
2994}
2995
2996#[derive(Deserialize, Default)]
2997struct SoftwareJson {
2998 os: Option<String>,
2999 os_version: Option<String>,
3000 #[serde(default)]
3001 runtimes: Vec<Vec<String>>,
3002 #[serde(default)]
3003 frameworks: Vec<Vec<String>>,
3004 cuda_version: Option<String>,
3005 #[serde(default)]
3006 drivers: Vec<Vec<String>>,
3007}
3008
3009#[derive(Deserialize)]
3010struct ModelJson {
3011 #[serde(default)]
3012 model_id: String,
3013 #[serde(default)]
3014 family: String,
3015 parameters_b_x10: Option<u32>,
3016 context_length: Option<u32>,
3017 quantization: Option<String>,
3018 #[serde(default)]
3019 modalities: Vec<String>,
3020 tokens_per_sec: Option<u32>,
3021 loaded: Option<bool>,
3022}
3023
3024#[derive(Deserialize)]
3025struct ToolJson {
3026 #[serde(default)]
3027 tool_id: String,
3028 #[serde(default)]
3029 name: String,
3030 version: Option<String>,
3031 input_schema: Option<String>,
3032 output_schema: Option<String>,
3033 #[serde(default)]
3034 requires: Vec<String>,
3035 estimated_time_ms: Option<u32>,
3036 stateless: Option<bool>,
3037}
3038
3039#[derive(Deserialize, Default)]
3040struct LimitsJson {
3041 max_concurrent_requests: Option<u32>,
3042 max_tokens_per_request: Option<u32>,
3043 rate_limit_rpm: Option<u32>,
3044 max_batch_size: Option<u32>,
3045 max_input_bytes: Option<u32>,
3046 max_output_bytes: Option<u32>,
3047}
3048
3049#[derive(Deserialize, Default)]
3050struct CapabilityFilterJson {
3051 #[serde(default)]
3052 require_tags: Vec<String>,
3053 #[serde(default)]
3054 require_models: Vec<String>,
3055 #[serde(default)]
3056 require_tools: Vec<String>,
3057 min_memory_gb: Option<u32>,
3058 require_gpu: Option<bool>,
3059 gpu_vendor: Option<String>,
3060 min_vram_gb: Option<u32>,
3061 min_context_length: Option<u32>,
3062 #[serde(default)]
3063 require_modalities: Vec<String>,
3064}
3065
3066fn pair_vec(xs: Vec<Vec<String>>) -> Vec<(String, String)> {
3069 xs.into_iter()
3070 .filter_map(|mut p| {
3071 if p.len() >= 2 {
3072 Some((std::mem::take(&mut p[0]), std::mem::take(&mut p[1])))
3073 } else {
3074 None
3075 }
3076 })
3077 .collect()
3078}
3079
3080#[inline]
3086fn saturating_u16_cap(v: u32) -> u16 {
3087 v.min(u16::MAX as u32) as u16
3088}
3089
3090fn gpu_info_from_json(g: GpuJson) -> GpuInfo {
3091 let vendor = g
3092 .vendor
3093 .as_deref()
3094 .map(parse_gpu_vendor_cap)
3095 .unwrap_or(GpuVendor::Unknown);
3096 let mut info = GpuInfo::new(vendor, g.model, g.vram_gb);
3097 if let Some(cu) = g.compute_units {
3098 info = info.with_compute_units(saturating_u16_cap(cu));
3099 }
3100 if let Some(tc) = g.tensor_cores {
3101 info = info.with_tensor_cores(saturating_u16_cap(tc));
3102 }
3103 if let Some(tf) = g.fp16_tflops_x10 {
3104 let tf_capped = saturating_u16_cap(tf);
3118 info = info.with_fp16_tflops(tf_capped as f32 / 10.0);
3119 }
3120 info
3121}
3122
3123fn accelerator_from_json(a: AcceleratorJson) -> AcceleratorInfo {
3124 AcceleratorInfo {
3125 accel_type: parse_accelerator_type_cap(&a.kind),
3126 model: a.model,
3127 memory_gb: a.memory_gb.unwrap_or(0),
3128 tops_x10: a.tops_x10.map(saturating_u16_cap).unwrap_or(0),
3129 }
3130}
3131
3132fn hardware_from_json(h: HardwareJson) -> HardwareCapabilities {
3133 let mut hw = HardwareCapabilities::new();
3134 match (h.cpu_cores, h.cpu_threads) {
3135 (Some(c), Some(t)) => hw = hw.with_cpu(saturating_u16_cap(c), saturating_u16_cap(t)),
3136 (Some(c), None) => {
3137 let c16 = saturating_u16_cap(c);
3138 hw = hw.with_cpu(c16, c16);
3139 }
3140 _ => {}
3141 }
3142 if let Some(mb) = h.memory_gb {
3143 hw = hw.with_memory(mb);
3144 }
3145 if let Some(g) = h.gpu {
3146 hw = hw.with_gpu(gpu_info_from_json(g));
3147 }
3148 for g in h.additional_gpus {
3149 hw = hw.add_gpu(gpu_info_from_json(g));
3150 }
3151 if let Some(mb) = h.storage_gb {
3152 hw = hw.with_storage(mb);
3153 }
3154 if let Some(gbps) = h.network_gbps {
3155 hw = hw.with_network(gbps);
3156 }
3157 for a in h.accelerators {
3158 hw = hw.add_accelerator(accelerator_from_json(a));
3159 }
3160 hw
3161}
3162
3163fn software_from_json(s: SoftwareJson) -> SoftwareCapabilities {
3164 let mut sw = SoftwareCapabilities::new()
3165 .with_os(s.os.unwrap_or_default(), s.os_version.unwrap_or_default());
3166 for (k, v) in pair_vec(s.runtimes) {
3167 sw = sw.add_runtime(k, v);
3168 }
3169 for (k, v) in pair_vec(s.frameworks) {
3170 sw = sw.add_framework(k, v);
3171 }
3172 if let Some(c) = s.cuda_version {
3173 sw = sw.with_cuda(c);
3174 }
3175 sw.drivers = pair_vec(s.drivers);
3176 sw
3177}
3178
3179fn model_from_json(m: ModelJson) -> ModelCapability {
3180 let mut mc = ModelCapability::new(m.model_id, m.family);
3181 if let Some(p) = m.parameters_b_x10 {
3182 mc.parameters_b_x10 = p;
3183 }
3184 if let Some(c) = m.context_length {
3185 mc = mc.with_context_length(c);
3186 }
3187 if let Some(q) = m.quantization {
3188 mc = mc.with_quantization(q);
3189 }
3190 for modality in m.modalities {
3191 match parse_modality_cap(&modality) {
3192 Some(parsed) => mc = mc.add_modality(parsed),
3193 None => {
3194 tracing::warn!(
3195 modality = %modality,
3196 "announce_capabilities: unknown modality string (typo?), \
3197 skipping rather than the pre-fix silent fallback to Text — \
3198 advertising a Text capability the node doesn't actually \
3199 have produced wrong scheduling decisions on the receiver",
3200 );
3201 }
3202 }
3203 }
3204 if let Some(t) = m.tokens_per_sec {
3205 mc = mc.with_tokens_per_sec(t);
3206 }
3207 if let Some(l) = m.loaded {
3208 mc = mc.with_loaded(l);
3209 }
3210 mc
3211}
3212
3213fn tool_from_json(t: ToolJson) -> ToolCapability {
3214 let mut tc = ToolCapability::new(t.tool_id, t.name);
3215 if let Some(v) = t.version {
3216 tc = tc.with_version(v);
3217 }
3218 if let Some(s) = t.input_schema {
3219 tc = tc.with_input_schema(s);
3220 }
3221 if let Some(s) = t.output_schema {
3222 tc = tc.with_output_schema(s);
3223 }
3224 for r in t.requires {
3225 tc = tc.requires(r);
3226 }
3227 if let Some(ms) = t.estimated_time_ms {
3228 tc = tc.with_estimated_time(ms);
3229 }
3230 if let Some(st) = t.stateless {
3231 tc = tc.with_stateless(st);
3232 }
3233 tc
3234}
3235
3236fn limits_from_json(l: LimitsJson) -> ResourceLimits {
3237 let mut rl = ResourceLimits::new();
3238 if let Some(n) = l.max_concurrent_requests {
3239 rl = rl.with_max_concurrent(n);
3240 }
3241 if let Some(n) = l.max_tokens_per_request {
3242 rl = rl.with_max_tokens(n);
3243 }
3244 if let Some(n) = l.rate_limit_rpm {
3245 rl = rl.with_rate_limit(n);
3246 }
3247 if let Some(n) = l.max_batch_size {
3248 rl = rl.with_max_batch(n);
3249 }
3250 if let Some(n) = l.max_input_bytes {
3251 rl.max_input_bytes = n;
3252 }
3253 if let Some(n) = l.max_output_bytes {
3254 rl.max_output_bytes = n;
3255 }
3256 rl
3257}
3258
3259fn capability_set_from_json(caps: CapabilitySetJson) -> CapabilitySet {
3260 let mut cs = CapabilitySet::new();
3261 if let Some(h) = caps.hardware {
3262 cs = cs.with_hardware(hardware_from_json(h));
3263 }
3264 if let Some(s) = caps.software {
3265 cs = cs.with_software(software_from_json(s));
3266 }
3267 for m in caps.models {
3268 cs = cs.add_model(model_from_json(m));
3269 }
3270 for t in caps.tools {
3271 cs = cs.add_tool(tool_from_json(t));
3272 }
3273 for tag in caps.tags {
3281 if tag == TAG_SCOPE_SUBNET_LOCAL {
3282 cs = cs.with_subnet_local_scope();
3283 } else if let Some(id) = tag.strip_prefix(TAG_SCOPE_TENANT_PREFIX) {
3284 cs = cs.with_tenant_scope(id);
3285 } else if let Some(name) = tag.strip_prefix(TAG_SCOPE_REGION_PREFIX) {
3286 cs = cs.with_region_scope(name);
3287 } else {
3288 cs = cs.add_tag(tag);
3289 }
3290 }
3291 if let Some(l) = caps.limits {
3292 cs = cs.with_limits(limits_from_json(l));
3293 }
3294 cs
3295}
3296
3297fn capability_filter_from_json(f: CapabilityFilterJson) -> CapabilityFilter {
3298 let mut cf = CapabilityFilter::new();
3299 for t in f.require_tags {
3300 cf = cf.require_tag(t);
3301 }
3302 for m in f.require_models {
3303 cf = cf.require_model(m);
3304 }
3305 for t in f.require_tools {
3306 cf = cf.require_tool(t);
3307 }
3308 if let Some(mb) = f.min_memory_gb {
3309 cf = cf.with_min_memory(mb);
3310 }
3311 if f.require_gpu.unwrap_or(false) {
3312 cf = cf.require_gpu();
3313 }
3314 if let Some(v) = f.gpu_vendor {
3315 cf = cf.with_gpu_vendor(parse_gpu_vendor_cap(&v));
3316 }
3317 if let Some(mb) = f.min_vram_gb {
3318 cf = cf.with_min_vram(mb);
3319 }
3320 if let Some(n) = f.min_context_length {
3321 cf = cf.with_min_context(n);
3322 }
3323 for m in f.require_modalities {
3324 match parse_modality_cap(&m) {
3325 Some(parsed) => cf = cf.require_modality(parsed),
3326 None => {
3327 tracing::warn!(
3340 modality = %m,
3341 "find_nodes: unknown modality string in require_modalities \
3342 filter (typo?), dropping the constraint; the resulting \
3343 filter is too permissive — pre-fix it was silently \
3344 re-interpreted as `require Text`, which returned the \
3345 wrong nodes",
3346 );
3347 }
3348 }
3349 }
3350 cf
3351}
3352
3353pub(crate) const NET_ERR_CAPABILITY: c_int = -128;
3356
3357#[unsafe(no_mangle)]
3364pub unsafe extern "C" fn net_mesh_announce_capabilities(
3365 handle: *mut MeshNodeHandle,
3366 caps_json: *const c_char,
3367) -> c_int {
3368 if handle.is_null() || caps_json.is_null() {
3369 return NetError::NullPointer.into();
3370 }
3371 let h = unsafe { &*handle };
3372 let _op = match h.guard.try_enter() {
3373 Some(op) => op,
3374 None => return NetError::ShuttingDown.into(),
3375 };
3376 let Some(s) = (unsafe { c_str_to_string(caps_json) }) else {
3377 return NetError::InvalidUtf8.into();
3378 };
3379 let parsed: CapabilitySetJson = match serde_json::from_str(&s) {
3380 Ok(v) => v,
3381 Err(_) => return NetError::InvalidJson.into(),
3382 };
3383 let caps = capability_set_from_json(parsed);
3384 let node = h.inner.clone();
3385 match block_on(async move { node.announce_capabilities(caps).await }) {
3386 Ok(()) => 0,
3387 Err(_) => NET_ERR_CAPABILITY,
3388 }
3389}
3390
3391#[unsafe(no_mangle)]
3394pub unsafe extern "C" fn net_mesh_find_nodes(
3395 handle: *mut MeshNodeHandle,
3396 filter_json: *const c_char,
3397 out_json: *mut *mut c_char,
3398 out_len: *mut usize,
3399) -> c_int {
3400 if handle.is_null() || filter_json.is_null() || out_json.is_null() || out_len.is_null() {
3401 return NetError::NullPointer.into();
3402 }
3403 let h = unsafe { &*handle };
3404 let _op = match h.guard.try_enter() {
3405 Some(op) => op,
3406 None => return NetError::ShuttingDown.into(),
3407 };
3408 let Some(s) = (unsafe { c_str_to_string(filter_json) }) else {
3409 return NetError::InvalidUtf8.into();
3410 };
3411 let parsed: CapabilityFilterJson = match serde_json::from_str(&s) {
3412 Ok(v) => v,
3413 Err(_) => return NetError::InvalidJson.into(),
3414 };
3415 let filter = capability_filter_from_json(parsed);
3416 let ids = h.inner.find_nodes_by_filter(&filter);
3417 write_json_out(&ids, out_json, out_len)
3418}
3419
3420#[derive(serde::Deserialize)]
3437struct ScopeFilterJson {
3438 kind: String,
3439 #[serde(default)]
3440 tenant: Option<String>,
3441 #[serde(default)]
3442 tenants: Option<Vec<String>>,
3443 #[serde(default)]
3444 region: Option<String>,
3445 #[serde(default)]
3446 regions: Option<Vec<String>>,
3447}
3448
3449enum ScopeFilterOwned {
3455 Any,
3456 GlobalOnly,
3457 SameSubnet,
3458 Tenant(String),
3459 Tenants(Vec<String>),
3460 Region(String),
3461 Regions(Vec<String>),
3462}
3463
3464fn scope_filter_from_json(f: ScopeFilterJson) -> ScopeFilterOwned {
3465 match f.kind.as_str() {
3466 "any" => ScopeFilterOwned::Any,
3467 "global_only" | "globalOnly" => ScopeFilterOwned::GlobalOnly,
3468 "same_subnet" | "sameSubnet" => ScopeFilterOwned::SameSubnet,
3469 "tenant" => match f.tenant {
3470 Some(t) if !t.is_empty() => ScopeFilterOwned::Tenant(t),
3471 _ => ScopeFilterOwned::Any,
3472 },
3473 "tenants" => match f.tenants {
3474 Some(ts) => {
3480 let cleaned: Vec<String> = ts.into_iter().filter(|t| !t.is_empty()).collect();
3481 if cleaned.is_empty() {
3482 ScopeFilterOwned::Any
3483 } else {
3484 ScopeFilterOwned::Tenants(cleaned)
3485 }
3486 }
3487 None => ScopeFilterOwned::Any,
3488 },
3489 "region" => match f.region {
3490 Some(r) if !r.is_empty() => ScopeFilterOwned::Region(r),
3491 _ => ScopeFilterOwned::Any,
3492 },
3493 "regions" => match f.regions {
3494 Some(rs) => {
3496 let cleaned: Vec<String> = rs.into_iter().filter(|r| !r.is_empty()).collect();
3497 if cleaned.is_empty() {
3498 ScopeFilterOwned::Any
3499 } else {
3500 ScopeFilterOwned::Regions(cleaned)
3501 }
3502 }
3503 None => ScopeFilterOwned::Any,
3504 },
3505 _ => ScopeFilterOwned::Any,
3506 }
3507}
3508
3509fn with_scope_filter<R>(
3514 owned: &ScopeFilterOwned,
3515 f: impl FnOnce(&crate::adapter::net::behavior::capability::ScopeFilter<'_>) -> R,
3516) -> R {
3517 use crate::adapter::net::behavior::capability::ScopeFilter as F;
3518 match owned {
3519 ScopeFilterOwned::Any => f(&F::Any),
3520 ScopeFilterOwned::GlobalOnly => f(&F::GlobalOnly),
3521 ScopeFilterOwned::SameSubnet => f(&F::SameSubnet),
3522 ScopeFilterOwned::Tenant(t) => f(&F::Tenant(t.as_str())),
3523 ScopeFilterOwned::Tenants(ts) => {
3524 let refs: Vec<&str> = ts.iter().map(|s| s.as_str()).collect();
3525 f(&F::Tenants(refs.as_slice()))
3526 }
3527 ScopeFilterOwned::Region(r) => f(&F::Region(r.as_str())),
3528 ScopeFilterOwned::Regions(rs) => {
3529 let refs: Vec<&str> = rs.iter().map(|s| s.as_str()).collect();
3530 f(&F::Regions(refs.as_slice()))
3531 }
3532 }
3533}
3534
3535#[unsafe(no_mangle)]
3558pub unsafe extern "C" fn net_mesh_find_nodes_scoped(
3559 handle: *mut MeshNodeHandle,
3560 filter_json: *const c_char,
3561 scope_json: *const c_char,
3562 out_json: *mut *mut c_char,
3563 out_len: *mut usize,
3564) -> c_int {
3565 if handle.is_null()
3566 || filter_json.is_null()
3567 || scope_json.is_null()
3568 || out_json.is_null()
3569 || out_len.is_null()
3570 {
3571 return NetError::NullPointer.into();
3572 }
3573 let h = unsafe { &*handle };
3574 let _op = match h.guard.try_enter() {
3575 Some(op) => op,
3576 None => return NetError::ShuttingDown.into(),
3577 };
3578 let Some(filter_s) = (unsafe { c_str_to_string(filter_json) }) else {
3579 return NetError::InvalidUtf8.into();
3580 };
3581 let Some(scope_s) = (unsafe { c_str_to_string(scope_json) }) else {
3582 return NetError::InvalidUtf8.into();
3583 };
3584 let parsed_filter: CapabilityFilterJson = match serde_json::from_str(&filter_s) {
3585 Ok(v) => v,
3586 Err(_) => return NetError::InvalidJson.into(),
3587 };
3588 let parsed_scope: ScopeFilterJson = match serde_json::from_str(&scope_s) {
3589 Ok(v) => v,
3590 Err(_) => return NetError::InvalidJson.into(),
3591 };
3592 let filter = capability_filter_from_json(parsed_filter);
3593 let owned = scope_filter_from_json(parsed_scope);
3594 let ids = with_scope_filter(&owned, |sf| {
3595 h.inner.find_nodes_by_filter_scoped(&filter, sf)
3596 });
3597 write_json_out(&ids, out_json, out_len)
3598}
3599
3600#[derive(serde::Deserialize)]
3614struct CapabilityRequirementJson {
3615 #[serde(default)]
3616 filter: CapabilityFilterJson,
3617 #[serde(default)]
3618 prefer_more_memory: f32,
3619 #[serde(default)]
3620 prefer_more_vram: f32,
3621 #[serde(default)]
3622 prefer_faster_inference: f32,
3623 #[serde(default)]
3624 prefer_loaded_models: f32,
3625}
3626
3627fn capability_requirement_from_json(
3628 j: CapabilityRequirementJson,
3629) -> crate::adapter::net::behavior::capability::CapabilityRequirement {
3630 crate::adapter::net::behavior::capability::CapabilityRequirement::from_filter(
3631 capability_filter_from_json(j.filter),
3632 )
3633 .prefer_memory(j.prefer_more_memory)
3634 .prefer_vram(j.prefer_more_vram)
3635 .prefer_speed(j.prefer_faster_inference)
3636 .prefer_loaded(j.prefer_loaded_models)
3637}
3638
3639#[unsafe(no_mangle)]
3649pub unsafe extern "C" fn net_mesh_find_best_node(
3650 handle: *mut MeshNodeHandle,
3651 requirement_json: *const c_char,
3652 out_node_id: *mut u64,
3653 out_has_match: *mut c_int,
3654) -> c_int {
3655 if handle.is_null()
3656 || requirement_json.is_null()
3657 || out_node_id.is_null()
3658 || out_has_match.is_null()
3659 {
3660 return NetError::NullPointer.into();
3661 }
3662 let h = unsafe { &*handle };
3663 let _op = match h.guard.try_enter() {
3664 Some(op) => op,
3665 None => return NetError::ShuttingDown.into(),
3666 };
3667 let Some(s) = (unsafe { c_str_to_string(requirement_json) }) else {
3668 return NetError::InvalidUtf8.into();
3669 };
3670 let parsed: CapabilityRequirementJson = match serde_json::from_str(&s) {
3671 Ok(v) => v,
3672 Err(_) => return NetError::InvalidJson.into(),
3673 };
3674 let req = capability_requirement_from_json(parsed);
3675 match h.inner.find_best_node(&req) {
3676 Some(node_id) => unsafe {
3677 *out_node_id = node_id;
3678 *out_has_match = 1;
3679 },
3680 None => unsafe {
3681 *out_has_match = 0;
3682 },
3683 }
3684 0
3685}
3686
3687#[unsafe(no_mangle)]
3696pub unsafe extern "C" fn net_mesh_find_best_node_scoped(
3697 handle: *mut MeshNodeHandle,
3698 requirement_json: *const c_char,
3699 scope_json: *const c_char,
3700 out_node_id: *mut u64,
3701 out_has_match: *mut c_int,
3702) -> c_int {
3703 if handle.is_null()
3704 || requirement_json.is_null()
3705 || scope_json.is_null()
3706 || out_node_id.is_null()
3707 || out_has_match.is_null()
3708 {
3709 return NetError::NullPointer.into();
3710 }
3711 let h = unsafe { &*handle };
3712 let _op = match h.guard.try_enter() {
3713 Some(op) => op,
3714 None => return NetError::ShuttingDown.into(),
3715 };
3716 let Some(req_s) = (unsafe { c_str_to_string(requirement_json) }) else {
3717 return NetError::InvalidUtf8.into();
3718 };
3719 let Some(scope_s) = (unsafe { c_str_to_string(scope_json) }) else {
3720 return NetError::InvalidUtf8.into();
3721 };
3722 let parsed_req: CapabilityRequirementJson = match serde_json::from_str(&req_s) {
3723 Ok(v) => v,
3724 Err(_) => return NetError::InvalidJson.into(),
3725 };
3726 let parsed_scope: ScopeFilterJson = match serde_json::from_str(&scope_s) {
3727 Ok(v) => v,
3728 Err(_) => return NetError::InvalidJson.into(),
3729 };
3730 let req = capability_requirement_from_json(parsed_req);
3731 let owned = scope_filter_from_json(parsed_scope);
3732 let result = with_scope_filter(&owned, |sf| h.inner.find_best_node_scoped(&req, sf));
3733 match result {
3734 Some(node_id) => unsafe {
3735 *out_node_id = node_id;
3736 *out_has_match = 1;
3737 },
3738 None => unsafe {
3739 *out_has_match = 0;
3740 },
3741 }
3742 0
3743}
3744
3745#[unsafe(no_mangle)]
3747pub unsafe extern "C" fn net_normalize_gpu_vendor(
3748 raw: *const c_char,
3749 out_json: *mut *mut c_char,
3750 out_len: *mut usize,
3751) -> c_int {
3752 if raw.is_null() || out_json.is_null() || out_len.is_null() {
3753 return NetError::NullPointer.into();
3754 }
3755 let Some(s) = (unsafe { c_str_to_string(raw) }) else {
3756 return NetError::InvalidUtf8.into();
3757 };
3758 let canonical = gpu_vendor_to_string_cap(parse_gpu_vendor_cap(&s));
3759 write_string_out(canonical.to_string(), out_json, out_len)
3760}
3761
3762pub(crate) const NET_ERR_GANG_INVALID: c_int = -140;
3773
3774#[derive(Deserialize)]
3778struct GangCriteriaJson {
3779 #[serde(default)]
3781 tags_all: Vec<String>,
3782 #[serde(default)]
3783 tags_any: Vec<String>,
3784 #[serde(default)]
3785 tag_groups_all: Vec<Vec<String>>,
3786 #[serde(default)]
3788 region: Option<String>,
3789 #[serde(default)]
3791 min_units: usize,
3792 #[serde(default)]
3793 max_load: Option<f32>,
3794 #[serde(default)]
3795 max_p50_latency_us: Option<u32>,
3796 #[serde(default)]
3797 require_all: Vec<String>,
3798 #[serde(default)]
3799 require_any: Vec<String>,
3800 #[serde(default)]
3801 selection: Option<String>,
3802 #[serde(default)]
3803 load_band_target: Option<f32>,
3804 #[serde(default)]
3805 prefer_capability: Option<String>,
3806}
3807
3808#[derive(Deserialize)]
3811struct IslandRecordJson {
3812 id: u64,
3813 #[serde(default)]
3814 units: Vec<u32>,
3815 #[serde(default)]
3816 capabilities: Vec<String>,
3817 #[serde(default)]
3818 load: f32,
3819 #[serde(default)]
3820 p50_latency_us: u32,
3821}
3822
3823fn build_gang_criteria(
3824 c: GangCriteriaJson,
3825) -> Option<crate::adapter::net::behavior::gang::MatchCriteria> {
3826 use crate::adapter::net::behavior::fold::{CapabilityFilter, CapabilityQuery};
3827 use crate::adapter::net::behavior::gang::{MatchCriteria, NumericFilter, SelectionPolicy};
3828 let selection = match c.selection.as_deref() {
3829 None | Some("least_loaded") => SelectionPolicy::LeastLoaded,
3830 Some("pack") => SelectionPolicy::Pack,
3831 Some("lowest_id") => SelectionPolicy::LowestId,
3832 Some("load_band") => SelectionPolicy::LoadBand(c.load_band_target.unwrap_or(0.5)),
3833 Some(_) => return None,
3834 };
3835 Some(MatchCriteria {
3836 capability: CapabilityQuery::Composite(CapabilityFilter {
3837 tags_all: c.tags_all,
3838 tags_any: c.tags_any,
3839 tag_groups_all: c.tag_groups_all,
3840 region: c.region,
3841 ..Default::default()
3842 }),
3843 numeric: NumericFilter {
3844 min_units: c.min_units,
3845 max_load: c.max_load,
3846 max_p50_latency_us: c.max_p50_latency_us,
3847 require_all: c.require_all,
3848 require_any: c.require_any,
3849 },
3850 selection,
3851 prefer_capability: c.prefer_capability,
3852 })
3853}
3854
3855#[unsafe(no_mangle)]
3860pub unsafe extern "C" fn net_mesh_publish_island_topology(
3861 handle: *mut MeshNodeHandle,
3862 record_json: *const c_char,
3863 out_count: *mut usize,
3864) -> c_int {
3865 if handle.is_null() || record_json.is_null() {
3866 return NetError::NullPointer.into();
3867 }
3868 let h = unsafe { &*handle };
3869 let _op = match h.guard.try_enter() {
3870 Some(op) => op,
3871 None => return NetError::ShuttingDown.into(),
3872 };
3873 let Some(js) = (unsafe { c_str_to_string(record_json) }) else {
3874 return NetError::InvalidUtf8.into();
3875 };
3876 let rec: IslandRecordJson = match serde_json::from_str(&js) {
3877 Ok(r) => r,
3878 Err(_) => return NET_ERR_GANG_INVALID,
3879 };
3880 use crate::adapter::net::behavior::fold::{IslandRecord, UnitSet};
3881 let record = IslandRecord {
3882 id: rec.id,
3883 units: UnitSet::new(rec.units),
3884 host: 0, capabilities: rec.capabilities,
3886 load: rec.load,
3887 p50_latency_us: rec.p50_latency_us,
3888 };
3889 let node = h.inner.clone();
3890 match block_on(async move { node.publish_island_topology(record).await }) {
3891 Ok(n) => {
3892 if !out_count.is_null() {
3893 unsafe {
3894 *out_count = n;
3895 }
3896 }
3897 0
3898 }
3899 Err(e) => adapter_err_to_code(&e),
3900 }
3901}
3902
3903#[unsafe(no_mangle)]
3907pub unsafe extern "C" fn net_mesh_match_islands(
3908 handle: *mut MeshNodeHandle,
3909 criteria_json: *const c_char,
3910 out_ids: *mut u64,
3911 cap: usize,
3912 out_count: *mut usize,
3913) -> c_int {
3914 if handle.is_null() || criteria_json.is_null() || out_count.is_null() {
3915 return NetError::NullPointer.into();
3916 }
3917 let h = unsafe { &*handle };
3918 let _op = match h.guard.try_enter() {
3919 Some(op) => op,
3920 None => return NetError::ShuttingDown.into(),
3921 };
3922 let Some(js) = (unsafe { c_str_to_string(criteria_json) }) else {
3923 return NetError::InvalidUtf8.into();
3924 };
3925 let parsed: GangCriteriaJson = match serde_json::from_str(&js) {
3926 Ok(c) => c,
3927 Err(_) => return NET_ERR_GANG_INVALID,
3928 };
3929 let Some(criteria) = build_gang_criteria(parsed) else {
3930 return NET_ERR_GANG_INVALID;
3931 };
3932 let ids = h.inner.match_islands(&criteria);
3933 unsafe {
3934 *out_count = ids.len();
3935 if !out_ids.is_null() {
3936 let n = ids.len().min(cap);
3937 std::ptr::copy_nonoverlapping(ids.as_ptr(), out_ids, n);
3938 }
3939 }
3940 0
3941}
3942
3943#[unsafe(no_mangle)]
3946pub unsafe extern "C" fn net_mesh_reserve_island(
3947 handle: *mut MeshNodeHandle,
3948 island: u64,
3949 until_unix_us: u64,
3950 out_outcome: *mut c_int,
3951) -> c_int {
3952 if handle.is_null() || out_outcome.is_null() {
3953 return NetError::NullPointer.into();
3954 }
3955 let h = unsafe { &*handle };
3956 let _op = match h.guard.try_enter() {
3957 Some(op) => op,
3958 None => return NetError::ShuttingDown.into(),
3959 };
3960 let node = h.inner.clone();
3961 match block_on(async move { node.reserve_island(island, until_unix_us).await }) {
3962 Ok(outcome) => {
3963 unsafe {
3964 *out_outcome = claim_outcome_code(outcome);
3965 }
3966 0
3967 }
3968 Err(e) => adapter_err_to_code(&e),
3969 }
3970}
3971
3972#[unsafe(no_mangle)]
3975pub unsafe extern "C" fn net_mesh_release_island(
3976 handle: *mut MeshNodeHandle,
3977 island: u64,
3978 out_outcome: *mut c_int,
3979) -> c_int {
3980 if handle.is_null() || out_outcome.is_null() {
3981 return NetError::NullPointer.into();
3982 }
3983 let h = unsafe { &*handle };
3984 let _op = match h.guard.try_enter() {
3985 Some(op) => op,
3986 None => return NetError::ShuttingDown.into(),
3987 };
3988 let node = h.inner.clone();
3989 match block_on(async move { node.release_island(island).await }) {
3990 Ok(outcome) => {
3991 unsafe {
3992 *out_outcome = claim_outcome_code(outcome);
3993 }
3994 0
3995 }
3996 Err(e) => adapter_err_to_code(&e),
3997 }
3998}
3999
4000#[unsafe(no_mangle)]
4004pub unsafe extern "C" fn net_mesh_claim_island(
4005 handle: *mut MeshNodeHandle,
4006 criteria_json: *const c_char,
4007 until_unix_us: u64,
4008 out_found: *mut c_int,
4009 out_island: *mut u64,
4010) -> c_int {
4011 if handle.is_null() || criteria_json.is_null() || out_found.is_null() || out_island.is_null() {
4012 return NetError::NullPointer.into();
4013 }
4014 unsafe {
4019 *out_found = 0;
4020 *out_island = 0;
4021 }
4022 let h = unsafe { &*handle };
4023 let _op = match h.guard.try_enter() {
4024 Some(op) => op,
4025 None => return NetError::ShuttingDown.into(),
4026 };
4027 let Some(js) = (unsafe { c_str_to_string(criteria_json) }) else {
4028 return NetError::InvalidUtf8.into();
4029 };
4030 let parsed: GangCriteriaJson = match serde_json::from_str(&js) {
4031 Ok(c) => c,
4032 Err(_) => return NET_ERR_GANG_INVALID,
4033 };
4034 let Some(criteria) = build_gang_criteria(parsed) else {
4035 return NET_ERR_GANG_INVALID;
4036 };
4037 let node = h.inner.clone();
4038 match block_on(async move { node.claim_island(&criteria, until_unix_us).await }) {
4039 Ok(Some(id)) => {
4040 unsafe {
4041 *out_found = 1;
4042 *out_island = id;
4043 }
4044 0
4045 }
4046 Ok(None) => 0,
4047 Err(e) => adapter_err_to_code(&e),
4048 }
4049}
4050
4051fn claim_outcome_code(o: crate::adapter::net::behavior::gang::ClaimOutcome) -> c_int {
4052 use crate::adapter::net::behavior::gang::ClaimOutcome;
4053 match o {
4054 ClaimOutcome::Won => 0,
4055 ClaimOutcome::Lost => 1,
4056 }
4057}
4058
4059#[cfg(test)]
4060mod tests {
4061 use super::*;
4062
4063 #[cfg(feature = "nat-traversal")]
4071 mod traversal_stats_abi {
4072 use super::super::NetTraversalStatsV2;
4073 use std::mem::{align_of, offset_of, size_of};
4074
4075 fn rust_offset(name: &str) -> Option<usize> {
4080 Some(match name {
4081 "punches_attempted" => offset_of!(NetTraversalStatsV2, punches_attempted),
4082 "punches_succeeded" => offset_of!(NetTraversalStatsV2, punches_succeeded),
4083 "punches_failed" => offset_of!(NetTraversalStatsV2, punches_failed),
4084 "relay_fallbacks" => offset_of!(NetTraversalStatsV2, relay_fallbacks),
4085 "punch_timeouts" => offset_of!(NetTraversalStatsV2, punch_timeouts),
4086 "punch_rejections" => offset_of!(NetTraversalStatsV2, punch_rejections),
4087 "rendezvous_no_relay" => offset_of!(NetTraversalStatsV2, rendezvous_no_relay),
4088 "upgrades_attempted" => offset_of!(NetTraversalStatsV2, upgrades_attempted),
4089 "upgrades_succeeded" => offset_of!(NetTraversalStatsV2, upgrades_succeeded),
4090 "upgrades_deferred_busy" => offset_of!(NetTraversalStatsV2, upgrades_deferred_busy),
4091 "port_mapping_renewals" => offset_of!(NetTraversalStatsV2, port_mapping_renewals),
4092 "port_mapping_active" => offset_of!(NetTraversalStatsV2, port_mapping_active),
4093 "port_mapping_external" => offset_of!(NetTraversalStatsV2, port_mapping_external),
4094 _ => return None,
4095 })
4096 }
4097
4098 fn c_type_layout(ctype: &str) -> (usize, usize) {
4108 use std::mem::{align_of, size_of};
4109 use std::os::raw::c_char;
4110 match ctype {
4111 "uint64_t" => (size_of::<u64>(), align_of::<u64>()),
4112 "uint8_t" => (size_of::<u8>(), align_of::<u8>()),
4113 "char[64]" => (size_of::<c_char>() * 64, align_of::<c_char>()),
4114 other => panic!("unhandled C type in net_traversal_stats_v2_t: {other:?}"),
4115 }
4116 }
4117
4118 fn round_up(off: usize, align: usize) -> usize {
4119 off.div_ceil(align) * align
4120 }
4121
4122 fn parse_header_fields(header: &str) -> Vec<(String, String)> {
4126 let end = header
4127 .find("} net_traversal_stats_v2_t;")
4128 .expect("stats typedef present in header");
4129 let open = header[..end].rfind('{').expect("struct open brace");
4130 let mut fields = Vec::new();
4131 for line in header[open + 1..end].lines() {
4132 let line = line.trim();
4133 if line.is_empty()
4134 || line.starts_with("//")
4135 || line.starts_with('*')
4136 || line.starts_with("/*")
4137 {
4138 continue;
4139 }
4140 let decl = line.trim_end_matches(';').trim();
4141 let (ctype, name_arr) = decl
4142 .rsplit_once(char::is_whitespace)
4143 .expect("field decl shaped `type name`");
4144 let (ctype, name_arr) = (ctype.trim(), name_arr.trim());
4145 if let Some((name, arr)) = name_arr.split_once('[') {
4146 fields.push((format!("{ctype}[{arr}"), name.to_string()));
4147 } else {
4148 fields.push((ctype.to_string(), name_arr.to_string()));
4149 }
4150 }
4151 fields
4152 }
4153
4154 #[test]
4155 fn c_header_layout_matches_rust_repr_c() {
4156 let header =
4157 std::fs::read_to_string(concat!(env!("CARGO_MANIFEST_DIR"), "/include/net.go.h"))
4158 .expect("read include/net.go.h");
4159 let fields = parse_header_fields(&header);
4160 assert_eq!(
4161 fields.len(),
4162 13,
4163 "expected 13 fields in net_traversal_stats_v2_t, parsed {fields:?}",
4164 );
4165
4166 let mut off = 0usize;
4171 let mut align = 1usize;
4172 for (ctype, name) in &fields {
4173 let (sz, al) = c_type_layout(ctype);
4174 off = round_up(off, al);
4175 align = align.max(al);
4176 let rust = rust_offset(name)
4177 .unwrap_or_else(|| panic!("header field `{name}` has no Rust struct field"));
4178 assert_eq!(
4179 rust, off,
4180 "field `{name}`: Rust offset {rust} != C offset {off}"
4181 );
4182 off += sz;
4183 }
4184 assert_eq!(
4185 size_of::<NetTraversalStatsV2>(),
4186 round_up(off, align),
4187 "net_traversal_stats_v2_t total size drift (Rust vs C header)",
4188 );
4189 assert_eq!(
4190 align_of::<NetTraversalStatsV2>(),
4191 align,
4192 "net_traversal_stats_v2_t alignment drift (Rust vs C header)",
4193 );
4194 }
4195 }
4196
4197 #[test]
4209 fn saturating_u16_cap_clamps_at_u16_max() {
4210 assert_eq!(saturating_u16_cap(0), 0);
4211 assert_eq!(saturating_u16_cap(42), 42);
4212 assert_eq!(saturating_u16_cap(u16::MAX as u32), u16::MAX);
4213 assert_eq!(saturating_u16_cap(u16::MAX as u32 + 1), u16::MAX);
4214 assert_eq!(saturating_u16_cap(u32::MAX), u16::MAX);
4215 }
4216
4217 #[test]
4223 fn parse_peer_pubkey_hex_accepts_valid_and_rejects_malformed() {
4224 use std::ffi::CString;
4225
4226 let valid = CString::new("ab".repeat(32)).unwrap();
4227 let parsed = unsafe { parse_peer_pubkey_hex(valid.as_ptr()) };
4229 assert_eq!(parsed, Ok([0xABu8; 32]), "64-char hex round-trips");
4230
4231 let bad_hex = CString::new("zz".repeat(32)).unwrap();
4232 let err = unsafe { parse_peer_pubkey_hex(bad_hex.as_ptr()) };
4234 assert_eq!(err, Err(NET_ERR_MESH_HANDSHAKE), "non-hex rejects");
4235
4236 let short = CString::new("abcd").unwrap();
4237 let err = unsafe { parse_peer_pubkey_hex(short.as_ptr()) };
4239 assert_eq!(err, Err(NET_ERR_MESH_HANDSHAKE), "wrong length rejects");
4240
4241 let non_utf8 = CString::new(vec![0xFFu8, 0xFEu8]).unwrap();
4243 let err = unsafe { parse_peer_pubkey_hex(non_utf8.as_ptr()) };
4245 assert_eq!(
4246 err,
4247 Err(NetError::InvalidUtf8.into()),
4248 "non-UTF-8 C string rejects with the UTF-8 code",
4249 );
4250 }
4251
4252 #[cfg(feature = "nat-traversal")]
4259 #[test]
4260 fn traversal_stats_v2_fill_maps_all_fields() {
4261 use crate::adapter::net::traversal::TraversalStatsSnapshot;
4262
4263 let snap = TraversalStatsSnapshot {
4264 punches_attempted: 1,
4265 punches_succeeded: 2,
4266 relay_fallbacks: 3,
4267 port_mapping_active: true,
4268 port_mapping_external: Some("203.0.113.5:4321".parse().unwrap()),
4269 port_mapping_renewals: 4,
4270 upgrades_attempted: 5,
4271 upgrades_succeeded: 6,
4272 upgrades_deferred_busy: 7,
4273 punches_failed: 8,
4274 punch_timeouts: 9,
4275 punch_rejections: 10,
4276 rendezvous_no_relay: 11,
4277 };
4278 let mut out = NetTraversalStatsV2 {
4279 punches_attempted: 0,
4280 punches_succeeded: 0,
4281 punches_failed: 0,
4282 relay_fallbacks: 0,
4283 punch_timeouts: 0,
4284 punch_rejections: 0,
4285 rendezvous_no_relay: 0,
4286 upgrades_attempted: 0,
4287 upgrades_succeeded: 0,
4288 upgrades_deferred_busy: 0,
4289 port_mapping_renewals: 0,
4290 port_mapping_active: 0,
4291 port_mapping_external: [0x7F; 64], };
4293 fill_traversal_stats_v2(&snap, &mut out);
4294
4295 assert_eq!(out.punches_attempted, 1);
4296 assert_eq!(out.punches_succeeded, 2);
4297 assert_eq!(out.relay_fallbacks, 3);
4298 assert_eq!(out.port_mapping_renewals, 4);
4299 assert_eq!(out.upgrades_attempted, 5);
4300 assert_eq!(out.upgrades_succeeded, 6);
4301 assert_eq!(out.upgrades_deferred_busy, 7);
4302 assert_eq!(out.punches_failed, 8);
4303 assert_eq!(out.punch_timeouts, 9);
4304 assert_eq!(out.punch_rejections, 10);
4305 assert_eq!(out.rendezvous_no_relay, 11);
4306 assert_eq!(out.port_mapping_active, 1);
4307 let s: String = out
4308 .port_mapping_external
4309 .iter()
4310 .take_while(|&&c| c != 0)
4311 .map(|&c| c as u8 as char)
4312 .collect();
4313 assert_eq!(s, "203.0.113.5:4321");
4314 assert!(out.port_mapping_external.contains(&0));
4316
4317 let snap_off = TraversalStatsSnapshot {
4319 port_mapping_active: false,
4320 port_mapping_external: None,
4321 ..snap
4322 };
4323 fill_traversal_stats_v2(&snap_off, &mut out);
4324 assert_eq!(out.port_mapping_active, 0);
4325 assert_eq!(
4326 out.port_mapping_external[0], 0,
4327 "empty string when inactive"
4328 );
4329 }
4330
4331 #[test]
4340 fn parse_modality_cap_returns_none_on_unknown_strings() {
4341 for (s, expected) in [
4343 ("text", Modality::Text),
4344 ("Text", Modality::Text),
4345 ("TEXT", Modality::Text),
4346 ("image", Modality::Image),
4347 ("audio", Modality::Audio),
4348 ("video", Modality::Video),
4349 ("code", Modality::Code),
4350 ("embedding", Modality::Embedding),
4351 ("tool-use", Modality::ToolUse),
4352 ("tool_use", Modality::ToolUse),
4353 ("tooluse", Modality::ToolUse),
4354 ] {
4355 assert_eq!(
4356 parse_modality_cap(s),
4357 Some(expected),
4358 "known modality `{s}` must parse",
4359 );
4360 }
4361
4362 for s in ["audoi", "imageX", "vidoe", "embeding", "garbage", ""] {
4364 assert_eq!(
4365 parse_modality_cap(s),
4366 None,
4367 "unknown modality `{s}` must return None — pre-fix this \
4368 fell back to Modality::Text, advertising a capability \
4369 the node didn't actually have",
4370 );
4371 }
4372 }
4373
4374 #[test]
4384 fn gpu_info_from_json_saturates_fp16_tflops_to_u16_max() {
4385 let g = GpuJson {
4388 vendor: None,
4389 model: "test".to_string(),
4390 vram_gb: 0,
4391 compute_units: None,
4392 tensor_cores: None,
4393 fp16_tflops_x10: Some(1_000_000_000u32),
4394 };
4395 let info = gpu_info_from_json(g);
4396 assert_eq!(
4400 info.fp16_tflops_x10,
4401 u16::MAX as u32,
4402 "fp16_tflops_x10 must saturate at u16::MAX (65535) instead of \
4403 losing precision through the f32 round-trip; got {}",
4404 info.fp16_tflops_x10,
4405 );
4406
4407 let g_small = GpuJson {
4409 vendor: None,
4410 model: "test".to_string(),
4411 vram_gb: 0,
4412 compute_units: None,
4413 tensor_cores: None,
4414 fp16_tflops_x10: Some(425), };
4416 let info_small = gpu_info_from_json(g_small);
4417 assert_eq!(
4418 info_small.fp16_tflops_x10, 425,
4419 "small fp16_tflops_x10 must round-trip exactly"
4420 );
4421 }
4422
4423 #[test]
4436 fn alloc_bytes_round_trip_across_sizes() {
4437 for size in [0usize, 1, 15, 16, 17, 32, 64, 1024, 8192] {
4438 let src: Vec<u8> = (0..size).map(|i| (i as u8).wrapping_mul(37)).collect();
4439 let mut ptr: *mut u8 = std::ptr::null_mut();
4440 let mut len: usize = 0;
4441 let rc = alloc_bytes(&src, &mut ptr as *mut _, &mut len as *mut _);
4442 assert_eq!(rc, 0);
4443 assert_eq!(len, size);
4444 if size == 0 {
4445 assert!(ptr.is_null());
4446 } else {
4447 assert!(!ptr.is_null());
4448 let observed = unsafe { std::slice::from_raw_parts(ptr, len) };
4449 assert_eq!(observed, &src[..]);
4450 }
4451 unsafe { net_free_bytes(ptr, len) };
4454 }
4455 }
4456
4457 #[test]
4458 fn net_free_bytes_null_and_zero_len_are_noops() {
4459 unsafe { net_free_bytes(std::ptr::null_mut(), 0) };
4461 unsafe { net_free_bytes(std::ptr::null_mut(), 42) };
4462 let mut sentinel: u8 = 0;
4465 unsafe { net_free_bytes(&mut sentinel as *mut u8, 0) };
4466 }
4467
4468 #[test]
4480 fn net_free_bytes_does_not_panic_on_oversized_len() {
4481 let mut sentinel: u8 = 0;
4489 let ptr = &mut sentinel as *mut u8;
4490 unsafe { net_free_bytes(ptr, usize::MAX) };
4493 assert_eq!(sentinel, 0, "sentinel must not have been written through");
4496 }
4497
4498 #[test]
4507 fn net_mesh_shutdown_runs_even_with_outstanding_arc_refs() {
4508 let cfg = serde_json::json!({
4509 "bind_addr": "127.0.0.1:0",
4510 "psk_hex": "0".repeat(64),
4511 });
4512 let cfg_c = CString::new(cfg.to_string()).unwrap();
4513 let mut out: *mut MeshNodeHandle = std::ptr::null_mut();
4514 let rc = unsafe { net_mesh_new(cfg_c.as_ptr(), &mut out) };
4515 assert_eq!(rc, 0, "net_mesh_new failed: {rc}");
4516 assert!(!out.is_null());
4517
4518 let inner_clone = {
4521 let h = unsafe { &*out };
4522 Arc::clone(&h.inner)
4523 };
4524 assert!(Arc::strong_count(&inner_clone) >= 2);
4525 assert!(!inner_clone.is_shutdown());
4526
4527 let rc = unsafe { net_mesh_shutdown(out) };
4528 assert_eq!(rc, 0, "net_mesh_shutdown returned {rc}");
4529 assert!(
4530 inner_clone.is_shutdown(),
4531 "shutdown flag must be set even when extra Arc refs are outstanding"
4532 );
4533
4534 drop(inner_clone);
4535 unsafe { net_mesh_free(out) };
4539 }
4540
4541 #[test]
4553 fn handles_match_rejects_stream_node_mismatch() {
4554 fn make_node_handle() -> *mut MeshNodeHandle {
4555 let cfg = serde_json::json!({
4556 "bind_addr": "127.0.0.1:0",
4557 "psk_hex": "0".repeat(64),
4558 });
4559 let cfg_c = CString::new(cfg.to_string()).unwrap();
4560 let mut out: *mut MeshNodeHandle = std::ptr::null_mut();
4561 let rc = unsafe { net_mesh_new(cfg_c.as_ptr(), &mut out) };
4562 assert_eq!(rc, 0);
4563 assert!(!out.is_null());
4564 out
4565 }
4566
4567 let nh_a = make_node_handle();
4568 let nh_b = make_node_handle();
4569
4570 let sh_a = {
4578 let h = unsafe { &*nh_a };
4579 let node_clone: Arc<MeshNode> = Arc::clone(&h.inner);
4580 MeshStreamHandle {
4581 stream: ManuallyDrop::new(CoreStream {
4582 peer_node_id: 0xDEAD,
4583 stream_id: 1,
4584 epoch: 0,
4585 config: StreamConfig::new(),
4586 }),
4587 _node: ManuallyDrop::new(node_clone),
4588 guard: HandleGuard::new(),
4589 }
4590 };
4591
4592 assert!(
4594 handles_match(&sh_a, unsafe { &*nh_a }),
4595 "stream from node_a + node_a handle must match"
4596 );
4597 assert!(
4599 !handles_match(&sh_a, unsafe { &*nh_b }),
4600 "stream from node_a + node_b handle must be rejected (#19)"
4601 );
4602
4603 unsafe {
4612 let mut sh_a = sh_a;
4613 let _ = ManuallyDrop::take(&mut sh_a.stream);
4614 let _ = ManuallyDrop::take(&mut sh_a._node);
4615 }
4616 unsafe { net_mesh_free(nh_a) };
4617 unsafe { net_mesh_free(nh_b) };
4618 }
4619
4620 #[test]
4627 fn net_mesh_free_is_idempotent() {
4628 let cfg = serde_json::json!({
4629 "bind_addr": "127.0.0.1:0",
4630 "psk_hex": "0".repeat(64),
4631 });
4632 let cfg_c = CString::new(cfg.to_string()).unwrap();
4633 let mut nh: *mut MeshNodeHandle = std::ptr::null_mut();
4634 assert_eq!(unsafe { net_mesh_new(cfg_c.as_ptr(), &mut nh) }, 0);
4635 assert!(!nh.is_null());
4636
4637 unsafe { net_mesh_free(nh) };
4638 unsafe { net_mesh_free(nh) };
4642 }
4643
4644 #[test]
4648 fn net_identity_free_is_idempotent() {
4649 let mut h: *mut IdentityHandle = std::ptr::null_mut();
4650 assert_eq!(unsafe { net_identity_generate(&mut h) }, 0);
4651 assert!(!h.is_null());
4652
4653 unsafe { net_identity_free(h) };
4654 unsafe { net_identity_free(h) };
4656 }
4657
4658 #[test]
4670 fn net_mesh_free_waits_for_inflight_op() {
4671 use std::sync::atomic::{AtomicBool, Ordering};
4672 use std::time::{Duration, Instant};
4673
4674 let cfg = serde_json::json!({
4675 "bind_addr": "127.0.0.1:0",
4676 "psk_hex": "0".repeat(64),
4677 });
4678 let cfg_c = CString::new(cfg.to_string()).unwrap();
4679 let mut nh: *mut MeshNodeHandle = std::ptr::null_mut();
4680 assert_eq!(unsafe { net_mesh_new(cfg_c.as_ptr(), &mut nh) }, 0);
4681 assert!(!nh.is_null());
4682
4683 let nh_addr = nh as usize;
4686 let started = Arc::new(AtomicBool::new(false));
4687 let release = Arc::new(AtomicBool::new(false));
4688 let started_w = started.clone();
4689 let release_w = release.clone();
4690
4691 let worker = std::thread::spawn(move || {
4692 let h = unsafe { &*(nh_addr as *mut MeshNodeHandle) };
4693 let op = h.guard.try_enter().expect("entry must succeed pre-free");
4697 started_w.store(true, Ordering::SeqCst);
4698 while !release_w.load(Ordering::SeqCst) {
4699 std::thread::sleep(Duration::from_millis(1));
4700 }
4701 drop(op);
4702 });
4703
4704 while !started.load(Ordering::SeqCst) {
4706 std::thread::yield_now();
4707 }
4708
4709 let release_clone = release.clone();
4712 std::thread::spawn(move || {
4713 std::thread::sleep(Duration::from_millis(50));
4714 release_clone.store(true, Ordering::SeqCst);
4715 });
4716
4717 let t0 = Instant::now();
4719 unsafe { net_mesh_free(nh) };
4720 let elapsed = t0.elapsed();
4721 assert!(
4722 elapsed >= Duration::from_millis(40),
4723 "net_mesh_free returned in {:?} — pre-fix it would have proceeded \
4724 immediately and the worker's subsequent op would UAF",
4725 elapsed,
4726 );
4727 worker.join().unwrap();
4728 }
4729
4730 #[test]
4737 fn net_mesh_stream_stats_returns_shutting_down_after_free() {
4738 let cfg = serde_json::json!({
4739 "bind_addr": "127.0.0.1:0",
4740 "psk_hex": "0".repeat(64),
4741 });
4742 let cfg_c = CString::new(cfg.to_string()).unwrap();
4743 let mut nh: *mut MeshNodeHandle = std::ptr::null_mut();
4744 assert_eq!(unsafe { net_mesh_new(cfg_c.as_ptr(), &mut nh) }, 0);
4745 assert!(!nh.is_null());
4746
4747 unsafe { net_mesh_free(nh) };
4750
4751 let mut out_json: *mut c_char = std::ptr::null_mut();
4752 let mut out_len: usize = 0;
4753 let rc = unsafe { net_mesh_stream_stats(nh, 0xDEAD, 1, &mut out_json, &mut out_len) };
4754 assert_eq!(
4755 rc,
4756 NetError::ShuttingDown as c_int,
4757 "post-free stream_stats must surface ShuttingDown (got {rc})",
4758 );
4759 assert!(
4760 out_json.is_null(),
4761 "no payload may be written after the guard fires",
4762 );
4763 }
4764
4765 #[test]
4770 fn net_identity_issue_token_returns_shutting_down_after_free() {
4771 let mut signer: *mut IdentityHandle = std::ptr::null_mut();
4772 assert_eq!(unsafe { net_identity_generate(&mut signer) }, 0);
4773 assert!(!signer.is_null());
4774 unsafe { net_identity_free(signer) };
4775
4776 let subject = [0u8; 32];
4779 let scope = CString::new("[\"publish\"]").unwrap();
4780 let channel = CString::new("test-channel").unwrap();
4781 let mut out_token: *mut u8 = std::ptr::null_mut();
4782 let mut out_token_len: usize = 0;
4783 let rc = unsafe {
4784 net_identity_issue_token(
4785 signer,
4786 subject.as_ptr(),
4787 subject.len(),
4788 scope.as_ptr(),
4789 channel.as_ptr(),
4790 60,
4791 0,
4792 &mut out_token,
4793 &mut out_token_len,
4794 )
4795 };
4796 assert_eq!(
4797 rc,
4798 NetError::ShuttingDown as c_int,
4799 "post-free issue_token must surface ShuttingDown (got {rc})",
4800 );
4801 assert!(out_token.is_null(), "no token bytes may be allocated");
4802 }
4803
4804 #[test]
4810 fn net_delegate_token_returns_shutting_down_after_free() {
4811 let mut signer: *mut IdentityHandle = std::ptr::null_mut();
4812 assert_eq!(unsafe { net_identity_generate(&mut signer) }, 0);
4813 assert!(!signer.is_null());
4814
4815 let subject = [0u8; 32];
4817 let scope = CString::new("[\"publish\",\"delegate\"]").unwrap();
4818 let channel = CString::new("test-channel").unwrap();
4819 let mut parent_bytes: *mut u8 = std::ptr::null_mut();
4820 let mut parent_len: usize = 0;
4821 assert_eq!(
4822 unsafe {
4823 net_identity_issue_token(
4824 signer,
4825 subject.as_ptr(),
4826 subject.len(),
4827 scope.as_ptr(),
4828 channel.as_ptr(),
4829 60,
4830 1,
4831 &mut parent_bytes,
4832 &mut parent_len,
4833 )
4834 },
4835 0,
4836 );
4837 assert!(!parent_bytes.is_null());
4838
4839 unsafe { net_identity_free(signer) };
4841
4842 let new_subject = [1u8; 32];
4843 let restricted = CString::new("[\"publish\"]").unwrap();
4844 let mut child_bytes: *mut u8 = std::ptr::null_mut();
4845 let mut child_len: usize = 0;
4846 let rc = unsafe {
4847 net_delegate_token(
4848 signer,
4849 parent_bytes,
4850 parent_len,
4851 new_subject.as_ptr(),
4852 new_subject.len(),
4853 restricted.as_ptr(),
4854 &mut child_bytes,
4855 &mut child_len,
4856 )
4857 };
4858 assert_eq!(
4859 rc,
4860 NetError::ShuttingDown as c_int,
4861 "post-free delegate_token must surface ShuttingDown (got {rc})",
4862 );
4863 assert!(child_bytes.is_null(), "no child token may be allocated");
4864
4865 unsafe { net_free_bytes(parent_bytes, parent_len) };
4867 }
4868
4869 #[test]
4870 fn hardware_from_json_saturates_overflow_cpu_fields() {
4871 let h = HardwareJson {
4874 cpu_cores: Some(70_000),
4875 cpu_threads: Some(200_000),
4876 memory_gb: None,
4877 gpu: None,
4878 additional_gpus: Vec::new(),
4879 storage_gb: None,
4880 network_gbps: None,
4881 accelerators: Vec::new(),
4882 };
4883 let hw = hardware_from_json(h);
4884 assert_eq!(hw.cpu_cores, u16::MAX);
4885 assert_eq!(hw.cpu_threads, u16::MAX);
4886 }
4887
4888 #[test]
4895 fn token_entry_points_reject_oversize_len() {
4896 let invalid_json: c_int = NetError::InvalidJson.into();
4897 let mut sentinel: u8 = 0;
4898 let token = &mut sentinel as *mut u8 as *const u8;
4899
4900 let mut out_json: *mut c_char = std::ptr::null_mut();
4901 let mut out_len: usize = 0;
4902 assert_eq!(
4903 unsafe { net_parse_token(token, usize::MAX, &mut out_json, &mut out_len) },
4904 invalid_json,
4905 );
4906 assert!(out_json.is_null());
4907
4908 let mut out_ok: c_int = -42;
4909 assert_eq!(
4910 unsafe { net_verify_token(token, usize::MAX, &mut out_ok) },
4911 invalid_json,
4912 );
4913
4914 let mut out_expired: c_int = -42;
4915 assert_eq!(
4916 unsafe { net_token_is_expired(token, usize::MAX, &mut out_expired) },
4917 invalid_json,
4918 );
4919
4920 assert_eq!(
4921 sentinel, 0,
4922 "sentinel must not be touched: the length guard fires before any deref"
4923 );
4924 }
4925}
4926
4927#[cfg(all(test, not(feature = "nat-traversal")))]
4928mod nat_traversal_stub_tests {
4929 use super::*;
4946 use std::ptr;
4947
4948 #[test]
4949 fn nat_type_stub_returns_unsupported() {
4950 let mut out_str: *mut c_char = ptr::null_mut();
4951 let mut out_len: usize = 0;
4952 let code = unsafe { net_mesh_nat_type(ptr::null_mut(), &mut out_str, &mut out_len) };
4955 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
4956 }
4957
4958 #[test]
4959 fn reflex_addr_stub_returns_unsupported() {
4960 let mut out_str: *mut c_char = ptr::null_mut();
4961 let mut out_len: usize = 0;
4962 let code = unsafe { net_mesh_reflex_addr(ptr::null_mut(), &mut out_str, &mut out_len) };
4964 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
4965 }
4966
4967 #[test]
4968 fn peer_nat_type_stub_returns_unsupported() {
4969 let mut out_str: *mut c_char = ptr::null_mut();
4970 let mut out_len: usize = 0;
4971 let code =
4973 unsafe { net_mesh_peer_nat_type(ptr::null_mut(), 0, &mut out_str, &mut out_len) };
4974 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
4975 }
4976
4977 #[test]
4978 fn probe_reflex_stub_returns_unsupported() {
4979 let mut out_str: *mut c_char = ptr::null_mut();
4980 let mut out_len: usize = 0;
4981 let code = unsafe { net_mesh_probe_reflex(ptr::null_mut(), 0, &mut out_str, &mut out_len) };
4983 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
4984 }
4985
4986 #[test]
4987 fn reclassify_nat_stub_returns_unsupported() {
4988 let code = unsafe { net_mesh_reclassify_nat(ptr::null_mut()) };
4990 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
4991 }
4992
4993 #[test]
4994 fn traversal_stats_stub_returns_unsupported() {
4995 let mut a: u64 = 0;
4996 let mut b: u64 = 0;
4997 let mut c: u64 = 0;
4998 let code = unsafe { net_mesh_traversal_stats(ptr::null_mut(), &mut a, &mut b, &mut c) };
5000 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
5001 }
5002
5003 #[test]
5004 fn connect_direct_stub_returns_unsupported() {
5005 let code = unsafe { net_mesh_connect_direct(ptr::null_mut(), 0, ptr::null(), 0) };
5007 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
5008 }
5009
5010 #[test]
5011 fn connect_direct_auto_stub_returns_unsupported() {
5012 let code = unsafe { net_mesh_connect_direct_auto(ptr::null_mut(), 0, ptr::null()) };
5014 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
5015 }
5016
5017 #[test]
5018 fn traversal_stats_v2_stub_returns_unsupported() {
5019 let code = unsafe { net_mesh_traversal_stats_v2(ptr::null_mut(), ptr::null_mut()) };
5021 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
5022 }
5023
5024 #[test]
5025 fn set_reflex_override_stub_returns_unsupported() {
5026 let code = unsafe { net_mesh_set_reflex_override(ptr::null_mut(), ptr::null()) };
5028 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
5029 }
5030
5031 #[test]
5032 fn clear_reflex_override_stub_returns_unsupported() {
5033 let code = unsafe { net_mesh_clear_reflex_override(ptr::null_mut()) };
5035 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
5036 }
5037
5038 #[test]
5044 fn unsupported_code_is_stable() {
5045 assert_eq!(NET_ERR_TRAVERSAL_UNSUPPORTED, -137);
5046 }
5047
5048 #[test]
5052 fn capability_set_from_go_marshal_preserves_gpu_vendor() {
5053 let json = r#"{"hardware":{"cpu_cores":16,"memory_gb":64,"gpu":{"vendor":"nvidia","model":"h100","vram_gb":80}},"tags":["gpu"]}"#;
5054 let parsed: CapabilitySetJson = serde_json::from_str(json).expect("JSON should parse");
5055 let caps = capability_set_from_json(parsed);
5056 let views = caps.views();
5060 assert_eq!(
5061 views.hardware().gpu_vendor(),
5062 Some(super::GpuVendor::Nvidia),
5063 "vendor lost in conversion"
5064 );
5065 assert_eq!(views.hardware().memory_gb, 64);
5066 assert_eq!(views.hardware().total_vram_gb(), 80);
5067 assert!(caps.has_tag("gpu"));
5068 }
5069
5070 #[test]
5079 fn collect_payloads_rejects_null_entry_with_nonzero_length() {
5080 let buf_a = b"hello".as_slice();
5081 let buf_b = b"world".as_slice();
5082 let ptrs: [*const u8; 3] = [buf_a.as_ptr(), std::ptr::null(), buf_b.as_ptr()];
5083 let lens: [usize; 3] = [buf_a.len(), 4, buf_b.len()];
5084
5085 let result = unsafe { collect_payloads(ptrs.as_ptr(), lens.as_ptr(), 3) };
5086 assert!(
5087 result.is_none(),
5088 "null entry with non-zero length must reject the whole batch"
5089 );
5090 }
5091
5092 #[test]
5093 fn collect_payloads_allows_null_entry_with_zero_length() {
5094 let buf_a = b"hello".as_slice();
5095 let ptrs: [*const u8; 2] = [buf_a.as_ptr(), std::ptr::null()];
5096 let lens: [usize; 2] = [buf_a.len(), 0];
5097
5098 let result = unsafe { collect_payloads(ptrs.as_ptr(), lens.as_ptr(), 2) }
5099 .expect("zero-length null is treated as empty payload");
5100 assert_eq!(result.len(), 2);
5101 assert_eq!(&result[0][..], b"hello");
5102 assert!(result[1].is_empty());
5103 }
5104
5105 #[test]
5106 fn collect_payloads_happy_path() {
5107 let buf_a = b"abc".as_slice();
5108 let buf_b = b"defg".as_slice();
5109 let ptrs: [*const u8; 2] = [buf_a.as_ptr(), buf_b.as_ptr()];
5110 let lens: [usize; 2] = [buf_a.len(), buf_b.len()];
5111
5112 let result = unsafe { collect_payloads(ptrs.as_ptr(), lens.as_ptr(), 2) }
5113 .expect("non-null entries should succeed");
5114 assert_eq!(result.len(), 2);
5115 assert_eq!(&result[0][..], b"abc");
5116 assert_eq!(&result[1][..], b"defg");
5117 }
5118}