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}
448
449pub struct MeshNodeHandle {
462 inner: ManuallyDrop<Arc<MeshNode>>,
463 channel_configs: ManuallyDrop<Arc<ChannelConfigRegistry>>,
464 guard: HandleGuard,
465}
466
467#[unsafe(no_mangle)]
482pub unsafe extern "C" fn net_mesh_new(
483 config_json: *const c_char,
484 out_handle: *mut *mut MeshNodeHandle,
485) -> c_int {
486 if config_json.is_null() || out_handle.is_null() {
487 return NetError::NullPointer.into();
488 }
489 let Some(s) = (unsafe { c_str_to_string(config_json) }) else {
490 return NetError::InvalidUtf8.into();
491 };
492 let cfg: MeshNewConfig = match serde_json::from_str(&s) {
493 Ok(v) => v,
494 Err(_) => return NetError::InvalidJson.into(),
495 };
496 let bind_addr: std::net::SocketAddr = match cfg.bind_addr.parse() {
497 Ok(a) => a,
498 Err(_) => return NET_ERR_MESH_INIT,
499 };
500 let psk_bytes = match hex::decode(&cfg.psk_hex) {
501 Ok(b) => b,
502 Err(_) => return NET_ERR_MESH_INIT,
503 };
504 if psk_bytes.len() != 32 {
505 return NET_ERR_MESH_INIT;
506 }
507 let mut psk = [0u8; 32];
508 psk.copy_from_slice(&psk_bytes);
509
510 let mut node_cfg = MeshNodeConfig::new(bind_addr, psk);
511 if let Some(ms) = cfg.heartbeat_ms {
519 if ms == 0 {
520 return NetError::InvalidJson.into();
521 }
522 node_cfg = node_cfg.with_heartbeat_interval(std::time::Duration::from_millis(ms));
523 }
524 if let Some(ms) = cfg.session_timeout_ms {
525 if ms == 0 {
526 return NetError::InvalidJson.into();
527 }
528 node_cfg = node_cfg.with_session_timeout(std::time::Duration::from_millis(ms));
529 }
530 if let Some(n) = cfg.num_shards {
531 node_cfg = node_cfg.with_num_shards(n);
532 }
533 if let Some(ms) = cfg.capability_gc_interval_ms {
534 node_cfg = node_cfg.with_capability_gc_interval(std::time::Duration::from_millis(ms));
535 }
536 if let Some(b) = cfg.require_signed_capabilities {
537 node_cfg = node_cfg.with_require_signed_capabilities(b);
538 }
539 if let Some(levels) = cfg.subnet {
540 let Some(id) = subnet_id_from_json(levels) else {
541 return NET_ERR_MESH_INIT;
542 };
543 node_cfg = node_cfg.with_subnet(id);
544 }
545 if let Some(policy_js) = cfg.subnet_policy {
546 let Some(policy) = subnet_policy_from_json(policy_js) else {
547 return NET_ERR_MESH_INIT;
548 };
549 node_cfg = node_cfg.with_subnet_policy(Arc::new(policy));
550 }
551 #[cfg(feature = "nat-traversal")]
552 if let Some(external_str) = cfg.reflex_override.as_deref() {
553 let Ok(external) = external_str.parse::<std::net::SocketAddr>() else {
554 return NET_ERR_MESH_INIT;
555 };
556 node_cfg = node_cfg.with_reflex_override(external);
557 }
558 #[cfg(not(feature = "nat-traversal"))]
562 let _ = cfg.reflex_override;
563 #[cfg(feature = "port-mapping")]
564 if cfg.try_port_mapping {
565 node_cfg = node_cfg.with_try_port_mapping(true);
566 }
567 #[cfg(not(feature = "port-mapping"))]
569 let _ = cfg.try_port_mapping;
570
571 let identity = match cfg.identity_seed_hex {
572 Some(seed_hex) => {
573 let bytes = match hex::decode(&seed_hex) {
574 Ok(b) => b,
575 Err(_) => return NET_ERR_MESH_INIT,
576 };
577 if bytes.len() != 32 {
578 return NET_ERR_MESH_INIT;
579 }
580 let mut arr = [0u8; 32];
581 arr.copy_from_slice(&bytes);
582 EntityKeypair::from_bytes(arr)
583 }
584 None => EntityKeypair::generate(),
585 };
586 let result = block_on(async move { MeshNode::new(identity, node_cfg).await });
587 match result {
588 Ok(mut node) => {
589 let channel_configs = Arc::new(ChannelConfigRegistry::new());
590 node.set_channel_configs(channel_configs.clone());
591 node.set_token_cache(Arc::new(TokenCache::new()));
595 let handle = Box::new(MeshNodeHandle {
596 inner: ManuallyDrop::new(Arc::new(node)),
597 channel_configs: ManuallyDrop::new(channel_configs),
598 guard: HandleGuard::new(),
599 });
600 unsafe {
601 *out_handle = Box::into_raw(handle);
602 }
603 0
604 }
605 Err(_) => NET_ERR_MESH_INIT,
606 }
607}
608
609#[unsafe(no_mangle)]
610pub unsafe extern "C" fn net_mesh_free(handle: *mut MeshNodeHandle) {
611 if handle.is_null() {
612 return;
613 }
614 let h: &MeshNodeHandle = unsafe { &*handle };
619 if h.guard.begin_free(FFI_HANDLE_FREE_DEADLINE) {
620 unsafe {
622 let mh = &mut *handle;
623 let inner = ManuallyDrop::take(&mut mh.inner);
624 let configs = ManuallyDrop::take(&mut mh.channel_configs);
625 drop(inner);
626 drop(configs);
627 }
628 } else {
629 tracing::warn!(
630 "net_mesh_free: in-flight ops did not drain within deadline; \
631 leaking inner to avoid use-after-free"
632 );
633 }
634}
635
636#[cfg(any(feature = "cortex", feature = "dataforts"))]
654pub(super) fn mesh_node_arc(h: &MeshNodeHandle) -> Option<Arc<MeshNode>> {
655 let _op = h.guard.try_enter()?;
656 Some(Arc::clone(&h.inner))
657}
658
659#[unsafe(no_mangle)]
667pub unsafe extern "C" fn net_mesh_arc_clone(handle: *mut MeshNodeHandle) -> *mut Arc<MeshNode> {
668 if handle.is_null() {
669 return std::ptr::null_mut();
670 }
671 let h = unsafe { &*handle };
672 let _op = match h.guard.try_enter() {
674 Some(op) => op,
675 None => return std::ptr::null_mut(),
676 };
677 let cloned: Arc<MeshNode> = Arc::clone(&h.inner);
678 Box::into_raw(Box::new(cloned))
679}
680
681#[unsafe(no_mangle)]
688pub unsafe extern "C" fn net_mesh_channel_configs_arc_clone(
689 handle: *mut MeshNodeHandle,
690) -> *mut Arc<ChannelConfigRegistry> {
691 if handle.is_null() {
692 return std::ptr::null_mut();
693 }
694 let h = unsafe { &*handle };
695 let _op = match h.guard.try_enter() {
697 Some(op) => op,
698 None => return std::ptr::null_mut(),
699 };
700 let cloned: Arc<ChannelConfigRegistry> = Arc::clone(&h.channel_configs);
701 Box::into_raw(Box::new(cloned))
702}
703
704#[unsafe(no_mangle)]
707pub unsafe extern "C" fn net_mesh_arc_free(p: *mut Arc<MeshNode>) {
708 if p.is_null() {
709 return;
710 }
711 unsafe {
712 drop(Box::from_raw(p));
713 }
714}
715
716#[unsafe(no_mangle)]
719pub unsafe extern "C" fn net_mesh_channel_configs_arc_free(p: *mut Arc<ChannelConfigRegistry>) {
720 if p.is_null() {
721 return;
722 }
723 unsafe {
724 drop(Box::from_raw(p));
725 }
726}
727
728#[unsafe(no_mangle)]
731pub unsafe extern "C" fn net_mesh_public_key_hex(
732 handle: *mut MeshNodeHandle,
733 out_ptr: *mut *mut c_char,
734 out_len: *mut usize,
735) -> c_int {
736 if handle.is_null() || out_ptr.is_null() || out_len.is_null() {
737 return NetError::NullPointer.into();
738 }
739 let h = unsafe { &*handle };
740 let _op = match h.guard.try_enter() {
741 Some(op) => op,
742 None => return NetError::ShuttingDown.into(),
743 };
744 let s = hex::encode(h.inner.public_key());
745 write_string_out(s, out_ptr, out_len)
746}
747
748#[unsafe(no_mangle)]
749pub unsafe extern "C" fn net_mesh_node_id(handle: *mut MeshNodeHandle) -> u64 {
750 if handle.is_null() {
751 return 0;
752 }
753 let h = unsafe { &*handle };
754 let _op = match h.guard.try_enter() {
756 Some(op) => op,
757 None => return 0,
758 };
759 h.inner.node_id()
760}
761
762#[unsafe(no_mangle)]
766pub unsafe extern "C" fn net_mesh_entity_id(handle: *mut MeshNodeHandle, out: *mut u8) -> c_int {
767 if handle.is_null() || out.is_null() {
768 return NetError::NullPointer.into();
769 }
770 let h = unsafe { &*handle };
771 let _op = match h.guard.try_enter() {
772 Some(op) => op,
773 None => return NetError::ShuttingDown.into(),
774 };
775 let bytes = h.inner.entity_id().as_bytes();
776 unsafe {
777 std::ptr::copy_nonoverlapping(bytes.as_ptr(), out, 32);
778 }
779 0
780}
781
782#[unsafe(no_mangle)]
784pub unsafe extern "C" fn net_mesh_connect(
785 handle: *mut MeshNodeHandle,
786 peer_addr: *const c_char,
787 peer_pubkey_hex: *const c_char,
788 peer_node_id: u64,
789) -> c_int {
790 if handle.is_null() || peer_addr.is_null() || peer_pubkey_hex.is_null() {
791 return NetError::NullPointer.into();
792 }
793 let h = unsafe { &*handle };
794 let _op = match h.guard.try_enter() {
795 Some(op) => op,
796 None => return NetError::ShuttingDown.into(),
797 };
798 let Some(addr_s) = (unsafe { c_str_to_string(peer_addr) }) else {
799 return NetError::InvalidUtf8.into();
800 };
801 let addr: std::net::SocketAddr = match addr_s.parse() {
802 Ok(a) => a,
803 Err(_) => return NET_ERR_MESH_HANDSHAKE,
804 };
805 let Some(pk_s) = (unsafe { c_str_to_string(peer_pubkey_hex) }) else {
806 return NetError::InvalidUtf8.into();
807 };
808 let pk_bytes = match hex::decode(pk_s) {
809 Ok(b) => b,
810 Err(_) => return NET_ERR_MESH_HANDSHAKE,
811 };
812 if pk_bytes.len() != 32 {
813 return NET_ERR_MESH_HANDSHAKE;
814 }
815 let mut pk = [0u8; 32];
816 pk.copy_from_slice(&pk_bytes);
817
818 let node = h.inner.clone();
819 match block_on(async move { node.connect(addr, &pk, peer_node_id).await }) {
820 Ok(_) => 0,
821 Err(e) => adapter_err_to_code(&e),
822 }
823}
824
825#[unsafe(no_mangle)]
828pub unsafe extern "C" fn net_mesh_accept(
829 handle: *mut MeshNodeHandle,
830 peer_node_id: u64,
831 out_addr: *mut *mut c_char,
832 out_len: *mut usize,
833) -> c_int {
834 if handle.is_null() || out_addr.is_null() || out_len.is_null() {
835 return NetError::NullPointer.into();
836 }
837 let h = unsafe { &*handle };
838 let _op = match h.guard.try_enter() {
839 Some(op) => op,
840 None => return NetError::ShuttingDown.into(),
841 };
842 let node = h.inner.clone();
843 match block_on(async move { node.accept(peer_node_id).await }) {
844 Ok((addr, _)) => write_string_out(addr.to_string(), out_addr, out_len),
845 Err(e) => adapter_err_to_code(&e),
846 }
847}
848
849#[unsafe(no_mangle)]
850pub unsafe extern "C" fn net_mesh_start(handle: *mut MeshNodeHandle) -> c_int {
851 if handle.is_null() {
852 return NetError::NullPointer.into();
853 }
854 let h = unsafe { &*handle };
855 let _op = match h.guard.try_enter() {
856 Some(op) => op,
857 None => return NetError::ShuttingDown.into(),
858 };
859 let node = h.inner.clone();
860 block_on(async move { node.start_arc() });
864 0
865}
866
867#[unsafe(no_mangle)]
879pub unsafe extern "C" fn net_mesh_shutdown(handle: *mut MeshNodeHandle) -> c_int {
880 if handle.is_null() {
881 return NetError::NullPointer.into();
882 }
883 let h = unsafe { &*handle };
884 let _op = match h.guard.try_enter() {
885 Some(op) => op,
886 None => return NetError::ShuttingDown.into(),
887 };
888 match block_on(async { h.inner.shutdown().await }) {
889 Ok(()) => 0,
890 Err(e) => adapter_err_to_code(&e),
891 }
892}
893
894#[cfg(feature = "nat-traversal")]
916#[unsafe(no_mangle)]
917pub unsafe extern "C" fn net_mesh_nat_type(
918 handle: *mut MeshNodeHandle,
919 out_str: *mut *mut c_char,
920 out_len: *mut usize,
921) -> c_int {
922 if handle.is_null() || out_str.is_null() || out_len.is_null() {
923 return NetError::NullPointer.into();
924 }
925 let h = unsafe { &*handle };
926 let _op = match h.guard.try_enter() {
927 Some(op) => op,
928 None => return NetError::ShuttingDown.into(),
929 };
930 write_string_out(
931 nat_class_to_str(h.inner.nat_class()).to_string(),
932 out_str,
933 out_len,
934 )
935}
936
937#[cfg(feature = "nat-traversal")]
942#[unsafe(no_mangle)]
943pub unsafe extern "C" fn net_mesh_reflex_addr(
944 handle: *mut MeshNodeHandle,
945 out_str: *mut *mut c_char,
946 out_len: *mut usize,
947) -> c_int {
948 if handle.is_null() || out_str.is_null() || out_len.is_null() {
949 return NetError::NullPointer.into();
950 }
951 let h = unsafe { &*handle };
952 let _op = match h.guard.try_enter() {
953 Some(op) => op,
954 None => return NetError::ShuttingDown.into(),
955 };
956 let s = h
957 .inner
958 .reflex_addr()
959 .map(|a| a.to_string())
960 .unwrap_or_default();
961 write_string_out(s, out_str, out_len)
962}
963
964#[cfg(feature = "nat-traversal")]
968#[unsafe(no_mangle)]
969pub unsafe extern "C" fn net_mesh_peer_nat_type(
970 handle: *mut MeshNodeHandle,
971 peer_node_id: u64,
972 out_str: *mut *mut c_char,
973 out_len: *mut usize,
974) -> c_int {
975 if handle.is_null() || out_str.is_null() || out_len.is_null() {
976 return NetError::NullPointer.into();
977 }
978 let h = unsafe { &*handle };
979 let _op = match h.guard.try_enter() {
980 Some(op) => op,
981 None => return NetError::ShuttingDown.into(),
982 };
983 write_string_out(
984 nat_class_to_str(h.inner.peer_nat_class(peer_node_id)).to_string(),
985 out_str,
986 out_len,
987 )
988}
989
990#[cfg(feature = "nat-traversal")]
999#[unsafe(no_mangle)]
1000pub unsafe extern "C" fn net_mesh_probe_reflex(
1001 handle: *mut MeshNodeHandle,
1002 peer_node_id: u64,
1003 out_str: *mut *mut c_char,
1004 out_len: *mut usize,
1005) -> c_int {
1006 if handle.is_null() || out_str.is_null() || out_len.is_null() {
1007 return NetError::NullPointer.into();
1008 }
1009 let h = unsafe { &*handle };
1010 let _op = match h.guard.try_enter() {
1011 Some(op) => op,
1012 None => return NetError::ShuttingDown.into(),
1013 };
1014 let node = h.inner.clone();
1015 match block_on(async move { node.probe_reflex(peer_node_id).await }) {
1016 Ok(addr) => write_string_out(addr.to_string(), out_str, out_len),
1017 Err(e) => traversal_err_to_code(&e),
1018 }
1019}
1020
1021#[cfg(feature = "nat-traversal")]
1026#[unsafe(no_mangle)]
1027pub unsafe extern "C" fn net_mesh_reclassify_nat(handle: *mut MeshNodeHandle) -> c_int {
1028 if handle.is_null() {
1029 return NetError::NullPointer.into();
1030 }
1031 let h = unsafe { &*handle };
1032 let _op = match h.guard.try_enter() {
1033 Some(op) => op,
1034 None => return NetError::ShuttingDown.into(),
1035 };
1036 let node = h.inner.clone();
1037 block_on(async move { node.reclassify_nat().await });
1038 0
1039}
1040
1041#[cfg(feature = "nat-traversal")]
1046#[unsafe(no_mangle)]
1047pub unsafe extern "C" fn net_mesh_traversal_stats(
1048 handle: *mut MeshNodeHandle,
1049 out_punches_attempted: *mut u64,
1050 out_punches_succeeded: *mut u64,
1051 out_relay_fallbacks: *mut u64,
1052) -> c_int {
1053 if handle.is_null() {
1054 return NetError::NullPointer.into();
1055 }
1056 let h = unsafe { &*handle };
1057 let _op = match h.guard.try_enter() {
1058 Some(op) => op,
1059 None => return NetError::ShuttingDown.into(),
1060 };
1061 let snap = h.inner.traversal_stats();
1062 unsafe {
1063 if !out_punches_attempted.is_null() {
1064 *out_punches_attempted = snap.punches_attempted;
1065 }
1066 if !out_punches_succeeded.is_null() {
1067 *out_punches_succeeded = snap.punches_succeeded;
1068 }
1069 if !out_relay_fallbacks.is_null() {
1070 *out_relay_fallbacks = snap.relay_fallbacks;
1071 }
1072 }
1073 0
1074}
1075
1076#[cfg(feature = "nat-traversal")]
1088#[unsafe(no_mangle)]
1089pub unsafe extern "C" fn net_mesh_connect_direct(
1090 handle: *mut MeshNodeHandle,
1091 peer_node_id: u64,
1092 peer_pubkey_hex: *const c_char,
1093 coordinator: u64,
1094) -> c_int {
1095 if handle.is_null() || peer_pubkey_hex.is_null() {
1096 return NetError::NullPointer.into();
1097 }
1098 let h = unsafe { &*handle };
1099 let _op = match h.guard.try_enter() {
1100 Some(op) => op,
1101 None => return NetError::ShuttingDown.into(),
1102 };
1103 let Some(pk_s) = (unsafe { c_str_to_string(peer_pubkey_hex) }) else {
1104 return NetError::InvalidUtf8.into();
1105 };
1106 let pk_bytes = match hex::decode(pk_s) {
1107 Ok(b) => b,
1108 Err(_) => return NET_ERR_MESH_HANDSHAKE,
1109 };
1110 if pk_bytes.len() != 32 {
1111 return NET_ERR_MESH_HANDSHAKE;
1112 }
1113 let mut pk = [0u8; 32];
1114 pk.copy_from_slice(&pk_bytes);
1115
1116 let node = h.inner.clone();
1117 match block_on(async move { node.connect_direct(peer_node_id, &pk, coordinator).await }) {
1118 Ok(_) => 0,
1119 Err(e) => traversal_err_to_code(&e),
1120 }
1121}
1122
1123#[cfg(feature = "nat-traversal")]
1131#[unsafe(no_mangle)]
1132pub unsafe extern "C" fn net_mesh_set_reflex_override(
1133 handle: *mut MeshNodeHandle,
1134 external: *const c_char,
1135) -> c_int {
1136 if handle.is_null() || external.is_null() {
1137 return NetError::NullPointer.into();
1138 }
1139 let h = unsafe { &*handle };
1140 let _op = match h.guard.try_enter() {
1141 Some(op) => op,
1142 None => return NetError::ShuttingDown.into(),
1143 };
1144 let Some(s) = (unsafe { c_str_to_string(external) }) else {
1145 return NetError::InvalidUtf8.into();
1146 };
1147 let Ok(addr) = s.parse::<std::net::SocketAddr>() else {
1148 return NET_ERR_MESH_INIT;
1149 };
1150 h.inner.set_reflex_override(addr);
1151 0
1152}
1153
1154#[cfg(feature = "nat-traversal")]
1162#[unsafe(no_mangle)]
1163pub unsafe extern "C" fn net_mesh_clear_reflex_override(handle: *mut MeshNodeHandle) -> c_int {
1164 if handle.is_null() {
1165 return NetError::NullPointer.into();
1166 }
1167 let h = unsafe { &*handle };
1168 let _op = match h.guard.try_enter() {
1169 Some(op) => op,
1170 None => return NetError::ShuttingDown.into(),
1171 };
1172 h.inner.clear_reflex_override();
1173 0
1174}
1175
1176#[cfg(not(feature = "nat-traversal"))]
1199#[unsafe(no_mangle)]
1200pub unsafe extern "C" fn net_mesh_nat_type(
1201 _handle: *mut MeshNodeHandle,
1202 _out_str: *mut *mut c_char,
1203 _out_len: *mut usize,
1204) -> c_int {
1205 NET_ERR_TRAVERSAL_UNSUPPORTED
1206}
1207
1208#[cfg(not(feature = "nat-traversal"))]
1209#[unsafe(no_mangle)]
1210pub unsafe extern "C" fn net_mesh_reflex_addr(
1211 _handle: *mut MeshNodeHandle,
1212 _out_str: *mut *mut c_char,
1213 _out_len: *mut usize,
1214) -> c_int {
1215 NET_ERR_TRAVERSAL_UNSUPPORTED
1216}
1217
1218#[cfg(not(feature = "nat-traversal"))]
1219#[unsafe(no_mangle)]
1220pub unsafe extern "C" fn net_mesh_peer_nat_type(
1221 _handle: *mut MeshNodeHandle,
1222 _peer_node_id: u64,
1223 _out_str: *mut *mut c_char,
1224 _out_len: *mut usize,
1225) -> c_int {
1226 NET_ERR_TRAVERSAL_UNSUPPORTED
1227}
1228
1229#[cfg(not(feature = "nat-traversal"))]
1230#[unsafe(no_mangle)]
1231pub unsafe extern "C" fn net_mesh_probe_reflex(
1232 _handle: *mut MeshNodeHandle,
1233 _peer_node_id: u64,
1234 _out_str: *mut *mut c_char,
1235 _out_len: *mut usize,
1236) -> c_int {
1237 NET_ERR_TRAVERSAL_UNSUPPORTED
1238}
1239
1240#[cfg(not(feature = "nat-traversal"))]
1241#[unsafe(no_mangle)]
1242pub unsafe extern "C" fn net_mesh_reclassify_nat(_handle: *mut MeshNodeHandle) -> c_int {
1243 NET_ERR_TRAVERSAL_UNSUPPORTED
1244}
1245
1246#[cfg(not(feature = "nat-traversal"))]
1247#[unsafe(no_mangle)]
1248pub unsafe extern "C" fn net_mesh_traversal_stats(
1249 _handle: *mut MeshNodeHandle,
1250 _out_punches_attempted: *mut u64,
1251 _out_punches_succeeded: *mut u64,
1252 _out_relay_fallbacks: *mut u64,
1253) -> c_int {
1254 NET_ERR_TRAVERSAL_UNSUPPORTED
1255}
1256
1257#[cfg(not(feature = "nat-traversal"))]
1258#[unsafe(no_mangle)]
1259pub unsafe extern "C" fn net_mesh_connect_direct(
1260 _handle: *mut MeshNodeHandle,
1261 _peer_node_id: u64,
1262 _peer_pubkey_hex: *const c_char,
1263 _coordinator: u64,
1264) -> c_int {
1265 NET_ERR_TRAVERSAL_UNSUPPORTED
1266}
1267
1268#[cfg(not(feature = "nat-traversal"))]
1269#[unsafe(no_mangle)]
1270pub unsafe extern "C" fn net_mesh_set_reflex_override(
1271 _handle: *mut MeshNodeHandle,
1272 _external: *const c_char,
1273) -> c_int {
1274 NET_ERR_TRAVERSAL_UNSUPPORTED
1275}
1276
1277#[cfg(not(feature = "nat-traversal"))]
1278#[unsafe(no_mangle)]
1279pub unsafe extern "C" fn net_mesh_clear_reflex_override(_handle: *mut MeshNodeHandle) -> c_int {
1280 NET_ERR_TRAVERSAL_UNSUPPORTED
1281}
1282
1283#[derive(Deserialize, Default)]
1288struct StreamOpenConfig {
1289 reliability: Option<String>,
1291 window_bytes: Option<u32>,
1294 fairness_weight: Option<u8>,
1295}
1296
1297pub struct MeshStreamHandle {
1312 stream: ManuallyDrop<CoreStream>,
1313 _node: ManuallyDrop<Arc<MeshNode>>,
1316 guard: HandleGuard,
1317}
1318
1319#[unsafe(no_mangle)]
1320pub unsafe extern "C" fn net_mesh_open_stream(
1321 handle: *mut MeshNodeHandle,
1322 peer_node_id: u64,
1323 stream_id: u64,
1324 config_json: *const c_char,
1325 out_stream: *mut *mut MeshStreamHandle,
1326) -> c_int {
1327 if handle.is_null() || out_stream.is_null() {
1328 return NetError::NullPointer.into();
1329 }
1330 let h = unsafe { &*handle };
1331 let _op = match h.guard.try_enter() {
1332 Some(op) => op,
1333 None => return NetError::ShuttingDown.into(),
1334 };
1335 let cfg_json: StreamOpenConfig = if config_json.is_null() {
1336 StreamOpenConfig::default()
1337 } else {
1338 let Some(s) = (unsafe { c_str_to_string(config_json) }) else {
1339 return NetError::InvalidUtf8.into();
1340 };
1341 match serde_json::from_str(&s) {
1342 Ok(v) => v,
1343 Err(_) => return NetError::InvalidJson.into(),
1344 }
1345 };
1346 let reliability = match cfg_json.reliability.as_deref() {
1347 None | Some("fire_and_forget") => Reliability::FireAndForget,
1348 Some("reliable") => Reliability::Reliable,
1349 Some(_) => return NET_ERR_MESH_TRANSPORT,
1350 };
1351 let window = cfg_json.window_bytes.unwrap_or(DEFAULT_STREAM_WINDOW_BYTES);
1352 let weight = cfg_json.fairness_weight.unwrap_or(1);
1353 let cfg = StreamConfig::new()
1354 .with_reliability(reliability)
1355 .with_window_bytes(window)
1356 .with_fairness_weight(weight);
1357 match h.inner.open_stream(peer_node_id, stream_id, cfg) {
1358 Ok(stream) => {
1359 let node_clone: Arc<MeshNode> = Arc::clone(&h.inner);
1360 let sh = Box::new(MeshStreamHandle {
1361 stream: ManuallyDrop::new(stream),
1362 _node: ManuallyDrop::new(node_clone),
1363 guard: HandleGuard::new(),
1364 });
1365 unsafe {
1366 *out_stream = Box::into_raw(sh);
1367 }
1368 0
1369 }
1370 Err(e) => adapter_err_to_code(&e),
1371 }
1372}
1373
1374#[unsafe(no_mangle)]
1375pub unsafe extern "C" fn net_mesh_stream_free(handle: *mut MeshStreamHandle) {
1376 if handle.is_null() {
1377 return;
1378 }
1379 let h: &MeshStreamHandle = unsafe { &*handle };
1381 if h.guard.begin_free(FFI_HANDLE_FREE_DEADLINE) {
1382 unsafe {
1384 let _stream = ManuallyDrop::take(&mut (*handle).stream);
1388 let node = ManuallyDrop::take(&mut (*handle)._node);
1389 drop(node);
1390 }
1391 } else {
1392 tracing::warn!(
1393 "net_mesh_stream_free: in-flight ops did not drain within deadline; \
1394 leaking inner to avoid use-after-free"
1395 );
1396 }
1397}
1398
1399unsafe fn collect_payloads(
1409 payloads: *const *const u8,
1410 lens: *const usize,
1411 count: usize,
1412) -> Option<Vec<Bytes>> {
1413 let mut out = Vec::with_capacity(count);
1414 for i in 0..count {
1415 let ptr = *payloads.add(i);
1416 let len = *lens.add(i);
1417 if ptr.is_null() {
1418 if len == 0 {
1419 out.push(Bytes::new());
1420 continue;
1421 }
1422 return None;
1423 }
1424 if len > isize::MAX as usize {
1428 return None;
1429 }
1430 let slice = std::slice::from_raw_parts(ptr, len);
1431 out.push(Bytes::copy_from_slice(slice));
1432 }
1433 Some(out)
1434}
1435
1436#[inline]
1444fn handles_match(sh: &MeshStreamHandle, nh: &MeshNodeHandle) -> bool {
1445 Arc::ptr_eq(&sh._node, &nh.inner)
1446}
1447
1448#[unsafe(no_mangle)]
1449pub unsafe extern "C" fn net_mesh_send(
1450 handle: *mut MeshStreamHandle,
1451 payloads: *const *const u8,
1452 lens: *const usize,
1453 count: usize,
1454 node_handle: *mut MeshNodeHandle,
1455) -> c_int {
1456 if handle.is_null() || node_handle.is_null() {
1457 return NetError::NullPointer.into();
1458 }
1459 if count > 0 && (payloads.is_null() || lens.is_null()) {
1460 return NetError::NullPointer.into();
1461 }
1462 let sh = unsafe { &*handle };
1463 let nh = unsafe { &*node_handle };
1464 let _sh_op = match sh.guard.try_enter() {
1467 Some(op) => op,
1468 None => return NetError::ShuttingDown.into(),
1469 };
1470 let _nh_op = match nh.guard.try_enter() {
1471 Some(op) => op,
1472 None => return NetError::ShuttingDown.into(),
1473 };
1474 if !handles_match(sh, nh) {
1475 return NetError::MismatchedHandles.into();
1476 }
1477 let payloads = match unsafe { collect_payloads(payloads, lens, count) } {
1478 Some(v) => v,
1479 None => return NetError::NullPointer.into(),
1480 };
1481 let node = nh.inner.clone();
1482 let stream = sh.stream.clone();
1483 match block_on(async move { node.send_on_stream(&stream, &payloads).await }) {
1484 Ok(()) => 0,
1485 Err(e) => stream_err_to_code(&e),
1486 }
1487}
1488
1489#[unsafe(no_mangle)]
1490pub unsafe extern "C" fn net_mesh_send_with_retry(
1491 handle: *mut MeshStreamHandle,
1492 payloads: *const *const u8,
1493 lens: *const usize,
1494 count: usize,
1495 max_retries: u32,
1496 node_handle: *mut MeshNodeHandle,
1497) -> c_int {
1498 if handle.is_null() || node_handle.is_null() {
1499 return NetError::NullPointer.into();
1500 }
1501 if count > 0 && (payloads.is_null() || lens.is_null()) {
1502 return NetError::NullPointer.into();
1503 }
1504 let sh = unsafe { &*handle };
1505 let nh = unsafe { &*node_handle };
1506 let _sh_op = match sh.guard.try_enter() {
1509 Some(op) => op,
1510 None => return NetError::ShuttingDown.into(),
1511 };
1512 let _nh_op = match nh.guard.try_enter() {
1513 Some(op) => op,
1514 None => return NetError::ShuttingDown.into(),
1515 };
1516 if !handles_match(sh, nh) {
1517 return NetError::MismatchedHandles.into();
1518 }
1519 let payloads = match unsafe { collect_payloads(payloads, lens, count) } {
1520 Some(v) => v,
1521 None => return NetError::NullPointer.into(),
1522 };
1523 let node = nh.inner.clone();
1524 let stream = sh.stream.clone();
1525 match block_on(async move {
1526 node.send_with_retry(&stream, &payloads, max_retries as usize)
1527 .await
1528 }) {
1529 Ok(()) => 0,
1530 Err(e) => stream_err_to_code(&e),
1531 }
1532}
1533
1534#[unsafe(no_mangle)]
1535pub unsafe extern "C" fn net_mesh_send_blocking(
1536 handle: *mut MeshStreamHandle,
1537 payloads: *const *const u8,
1538 lens: *const usize,
1539 count: usize,
1540 node_handle: *mut MeshNodeHandle,
1541) -> c_int {
1542 if handle.is_null() || node_handle.is_null() {
1543 return NetError::NullPointer.into();
1544 }
1545 if count > 0 && (payloads.is_null() || lens.is_null()) {
1546 return NetError::NullPointer.into();
1547 }
1548 let sh = unsafe { &*handle };
1549 let nh = unsafe { &*node_handle };
1550 let _sh_op = match sh.guard.try_enter() {
1553 Some(op) => op,
1554 None => return NetError::ShuttingDown.into(),
1555 };
1556 let _nh_op = match nh.guard.try_enter() {
1557 Some(op) => op,
1558 None => return NetError::ShuttingDown.into(),
1559 };
1560 if !handles_match(sh, nh) {
1561 return NetError::MismatchedHandles.into();
1562 }
1563 let payloads = match unsafe { collect_payloads(payloads, lens, count) } {
1564 Some(v) => v,
1565 None => return NetError::NullPointer.into(),
1566 };
1567 let node = nh.inner.clone();
1568 let stream = sh.stream.clone();
1569 match block_on(async move { node.send_blocking(&stream, &payloads).await }) {
1570 Ok(()) => 0,
1571 Err(e) => stream_err_to_code(&e),
1572 }
1573}
1574
1575#[derive(Serialize)]
1576struct StreamStatsJson {
1577 tx_seq: u64,
1578 rx_seq: u64,
1579 inbound_pending: u64,
1580 last_activity_ns: u64,
1581 active: bool,
1582 backpressure_events: u64,
1583 tx_credit_remaining: u32,
1584 tx_window: u32,
1585 credit_grants_received: u64,
1586 credit_grants_sent: u64,
1587}
1588
1589#[unsafe(no_mangle)]
1590pub unsafe extern "C" fn net_mesh_stream_stats(
1591 node_handle: *mut MeshNodeHandle,
1592 peer_node_id: u64,
1593 stream_id: u64,
1594 out_json: *mut *mut c_char,
1595 out_len: *mut usize,
1596) -> c_int {
1597 if node_handle.is_null() || out_json.is_null() || out_len.is_null() {
1598 return NetError::NullPointer.into();
1599 }
1600 let h = unsafe { &*node_handle };
1601 let _op = match h.guard.try_enter() {
1602 Some(op) => op,
1603 None => return NetError::ShuttingDown.into(),
1604 };
1605 match h.inner.stream_stats(peer_node_id, stream_id) {
1606 Some(s) => {
1607 let js = StreamStatsJson {
1608 tx_seq: s.tx_seq,
1609 rx_seq: s.rx_seq,
1610 inbound_pending: s.inbound_pending,
1611 last_activity_ns: s.last_activity_ns,
1612 active: s.active,
1613 backpressure_events: s.backpressure_events,
1614 tx_credit_remaining: s.tx_credit_remaining,
1615 tx_window: s.tx_window,
1616 credit_grants_received: s.credit_grants_received,
1617 credit_grants_sent: s.credit_grants_sent,
1618 };
1619 write_json_out(&js, out_json, out_len)
1620 }
1621 None => {
1622 write_string_out("null".to_string(), out_json, out_len)
1625 }
1626 }
1627}
1628
1629#[derive(Serialize)]
1634struct RecvEventJson {
1635 id: String,
1636 payload_b64: String,
1638 insertion_ts: u64,
1639 shard_id: u16,
1640}
1641
1642#[unsafe(no_mangle)]
1643pub unsafe extern "C" fn net_mesh_recv_shard(
1644 handle: *mut MeshNodeHandle,
1645 shard_id: u16,
1646 limit: u32,
1647 out_json: *mut *mut c_char,
1648 out_len: *mut usize,
1649) -> c_int {
1650 if handle.is_null() || out_json.is_null() || out_len.is_null() {
1651 return NetError::NullPointer.into();
1652 }
1653 let h = unsafe { &*handle };
1654 let _op = match h.guard.try_enter() {
1655 Some(op) => op,
1656 None => return NetError::ShuttingDown.into(),
1657 };
1658 let node = h.inner.clone();
1659 let result = block_on(async move { node.poll_shard(shard_id, None, limit as usize).await });
1660 let result = match result {
1661 Ok(r) => r,
1662 Err(e) => return adapter_err_to_code(&e),
1663 };
1664 let events: Vec<RecvEventJson> = result
1665 .events
1666 .into_iter()
1667 .map(|e| RecvEventJson {
1668 id: e.id,
1669 payload_b64: encode_b64(&e.raw),
1670 insertion_ts: e.insertion_ts,
1671 shard_id: e.shard_id,
1672 })
1673 .collect();
1674 write_json_out(&events, out_json, out_len)
1675}
1676
1677fn encode_b64(bytes: &[u8]) -> String {
1678 const ALPH: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
1681 let mut s = String::with_capacity(bytes.len().div_ceil(3) * 4);
1682 let mut i = 0;
1683 while i + 3 <= bytes.len() {
1684 let chunk = &bytes[i..i + 3];
1685 s.push(ALPH[(chunk[0] >> 2) as usize] as char);
1686 s.push(ALPH[(((chunk[0] & 0b11) << 4) | (chunk[1] >> 4)) as usize] as char);
1687 s.push(ALPH[(((chunk[1] & 0b1111) << 2) | (chunk[2] >> 6)) as usize] as char);
1688 s.push(ALPH[(chunk[2] & 0b111111) as usize] as char);
1689 i += 3;
1690 }
1691 let rem = bytes.len() - i;
1692 if rem == 1 {
1693 let b = bytes[i];
1694 s.push(ALPH[(b >> 2) as usize] as char);
1695 s.push(ALPH[((b & 0b11) << 4) as usize] as char);
1696 s.push('=');
1697 s.push('=');
1698 } else if rem == 2 {
1699 let b0 = bytes[i];
1700 let b1 = bytes[i + 1];
1701 s.push(ALPH[(b0 >> 2) as usize] as char);
1702 s.push(ALPH[(((b0 & 0b11) << 4) | (b1 >> 4)) as usize] as char);
1703 s.push(ALPH[((b1 & 0b1111) << 2) as usize] as char);
1704 s.push('=');
1705 }
1706 s
1707}
1708
1709#[derive(Deserialize)]
1714struct ChannelConfigInput {
1715 name: String,
1716 visibility: Option<String>,
1717 reliable: Option<bool>,
1718 require_token: Option<bool>,
1719 token_roots: Option<Vec<String>>,
1725 priority: Option<u8>,
1726 max_rate_pps: Option<u32>,
1727 publish_caps: Option<CapabilityFilterJson>,
1731 subscribe_caps: Option<CapabilityFilterJson>,
1735}
1736
1737fn parse_visibility(s: &str) -> Option<InnerVisibility> {
1738 match s {
1739 "subnet-local" => Some(InnerVisibility::SubnetLocal),
1740 "parent-visible" => Some(InnerVisibility::ParentVisible),
1741 "exported" => Some(InnerVisibility::Exported),
1742 "global" => Some(InnerVisibility::Global),
1743 _ => None,
1744 }
1745}
1746
1747#[unsafe(no_mangle)]
1748pub unsafe extern "C" fn net_mesh_register_channel(
1749 handle: *mut MeshNodeHandle,
1750 config_json: *const c_char,
1751) -> c_int {
1752 if handle.is_null() || config_json.is_null() {
1753 return NetError::NullPointer.into();
1754 }
1755 let h = unsafe { &*handle };
1756 let _op = match h.guard.try_enter() {
1757 Some(op) => op,
1758 None => return NetError::ShuttingDown.into(),
1759 };
1760 let Some(s) = (unsafe { c_str_to_string(config_json) }) else {
1761 return NetError::InvalidUtf8.into();
1762 };
1763 let input: ChannelConfigInput = match serde_json::from_str(&s) {
1764 Ok(v) => v,
1765 Err(_) => return NetError::InvalidJson.into(),
1766 };
1767 let name = match InnerChannelName::new(&input.name) {
1768 Ok(n) => n,
1769 Err(_) => return NET_ERR_CHANNEL,
1770 };
1771 let mut cfg = InnerChannelConfig::new(ChannelId::new(name));
1772 if let Some(v) = input.visibility {
1773 let Some(vis) = parse_visibility(&v) else {
1774 return NET_ERR_CHANNEL;
1775 };
1776 cfg = cfg.with_visibility(vis);
1777 }
1778 if let Some(r) = input.reliable {
1779 cfg = cfg.with_reliable(r);
1780 }
1781 if let Some(t) = input.require_token {
1782 cfg = cfg.with_require_token(t);
1783 }
1784 if let Some(roots) = input.token_roots {
1785 let mut parsed = Vec::with_capacity(roots.len());
1786 for hex_id in roots {
1787 let bytes = match hex::decode(&hex_id) {
1788 Ok(b) => b,
1789 Err(_) => return NET_ERR_CHANNEL,
1790 };
1791 let Ok(arr) = <[u8; 32]>::try_from(bytes.as_slice()) else {
1792 return NET_ERR_CHANNEL;
1793 };
1794 parsed.push(EntityId::from_bytes(arr));
1795 }
1796 cfg = cfg.with_token_roots(parsed);
1797 }
1798 if let Some(p) = input.priority {
1799 cfg = cfg.with_priority(p);
1800 }
1801 if let Some(pps) = input.max_rate_pps {
1802 cfg = cfg.with_rate_limit(pps);
1803 }
1804 if let Some(filter_json) = input.publish_caps {
1805 cfg = cfg.with_publish_caps(capability_filter_from_json(filter_json));
1806 }
1807 if let Some(filter_json) = input.subscribe_caps {
1808 cfg = cfg.with_subscribe_caps(capability_filter_from_json(filter_json));
1809 }
1810 h.channel_configs.insert(cfg);
1811 0
1812}
1813
1814#[unsafe(no_mangle)]
1815pub unsafe extern "C" fn net_mesh_subscribe_channel(
1816 handle: *mut MeshNodeHandle,
1817 publisher_node_id: u64,
1818 channel: *const c_char,
1819) -> c_int {
1820 subscribe_or_unsubscribe(handle, publisher_node_id, channel, true)
1821}
1822
1823#[unsafe(no_mangle)]
1824pub unsafe extern "C" fn net_mesh_unsubscribe_channel(
1825 handle: *mut MeshNodeHandle,
1826 publisher_node_id: u64,
1827 channel: *const c_char,
1828) -> c_int {
1829 subscribe_or_unsubscribe(handle, publisher_node_id, channel, false)
1830}
1831
1832#[unsafe(no_mangle)]
1839pub unsafe extern "C" fn net_mesh_subscribe_channel_with_token(
1840 handle: *mut MeshNodeHandle,
1841 publisher_node_id: u64,
1842 channel: *const c_char,
1843 token: *const u8,
1844 token_len: usize,
1845) -> c_int {
1846 if handle.is_null() || channel.is_null() || token.is_null() {
1847 return NetError::NullPointer.into();
1848 }
1849 let h = unsafe { &*handle };
1850 let _op = match h.guard.try_enter() {
1851 Some(op) => op,
1852 None => return NetError::ShuttingDown.into(),
1853 };
1854 let Some(s) = (unsafe { c_str_to_string(channel) }) else {
1855 return NetError::InvalidUtf8.into();
1856 };
1857 let name = match InnerChannelName::new(&s) {
1858 Ok(n) => n,
1859 Err(_) => return NET_ERR_CHANNEL,
1860 };
1861 if token_len > isize::MAX as usize {
1863 return NetError::InvalidJson.into();
1864 }
1865 let slice = unsafe { std::slice::from_raw_parts(token, token_len) };
1866 let parsed = match PermissionToken::from_bytes(slice) {
1867 Ok(t) => t,
1868 Err(e) => return token_err_to_code(&e),
1869 };
1870 let node = h.inner.clone();
1871 match block_on(async move {
1872 node.subscribe_channel_with_token(publisher_node_id, name, parsed)
1873 .await
1874 }) {
1875 Ok(()) => 0,
1876 Err(e) => adapter_err_to_channel_code(&e),
1877 }
1878}
1879
1880fn subscribe_or_unsubscribe(
1881 handle: *mut MeshNodeHandle,
1882 publisher_node_id: u64,
1883 channel: *const c_char,
1884 subscribe: bool,
1885) -> c_int {
1886 if handle.is_null() || channel.is_null() {
1887 return NetError::NullPointer.into();
1888 }
1889 let h = unsafe { &*handle };
1890 let _op = match h.guard.try_enter() {
1891 Some(op) => op,
1892 None => return NetError::ShuttingDown.into(),
1893 };
1894 let Some(s) = (unsafe { c_str_to_string(channel) }) else {
1895 return NetError::InvalidUtf8.into();
1896 };
1897 let name = match InnerChannelName::new(&s) {
1898 Ok(n) => n,
1899 Err(_) => return NET_ERR_CHANNEL,
1900 };
1901 let node = h.inner.clone();
1902 let outcome = if subscribe {
1903 block_on(async move { node.subscribe_channel(publisher_node_id, name).await })
1904 } else {
1905 block_on(async move { node.unsubscribe_channel(publisher_node_id, name).await })
1906 };
1907 match outcome {
1908 Ok(()) => 0,
1909 Err(e) => adapter_err_to_channel_code(&e),
1910 }
1911}
1912
1913fn adapter_err_to_channel_code(err: &AdapterError) -> c_int {
1914 if let AdapterError::Connection(msg) = err {
1915 let prefix = "membership request rejected: ";
1916 if let Some(tail) = msg.strip_prefix(prefix) {
1917 if tail.trim() == "Some(Unauthorized)" {
1918 return NET_ERR_CHANNEL_AUTH;
1919 }
1920 }
1921 }
1922 NET_ERR_CHANNEL
1923}
1924
1925#[derive(Deserialize, Default)]
1926struct PublishConfigInput {
1927 reliability: Option<String>,
1928 on_failure: Option<String>,
1929 max_inflight: Option<u32>,
1930}
1931
1932#[derive(Serialize)]
1933struct PublishReportJson {
1934 attempted: u32,
1935 delivered: u32,
1936 errors: Vec<PublishFailureJson>,
1937}
1938
1939#[derive(Serialize)]
1940struct PublishFailureJson {
1941 node_id: u64,
1942 message: String,
1943}
1944
1945fn to_publish_report_json(r: InnerPublishReport) -> PublishReportJson {
1946 PublishReportJson {
1947 attempted: r.attempted as u32,
1948 delivered: r.delivered as u32,
1949 errors: r
1950 .errors
1951 .into_iter()
1952 .map(|(id, e)| PublishFailureJson {
1953 node_id: id,
1954 message: format!("{}", e),
1955 })
1956 .collect(),
1957 }
1958}
1959
1960#[unsafe(no_mangle)]
1961pub unsafe extern "C" fn net_mesh_publish(
1962 handle: *mut MeshNodeHandle,
1963 channel: *const c_char,
1964 payload: *const u8,
1965 len: usize,
1966 config_json: *const c_char,
1967 out_json: *mut *mut c_char,
1968 out_len: *mut usize,
1969) -> c_int {
1970 if handle.is_null() || channel.is_null() || out_json.is_null() || out_len.is_null() {
1971 return NetError::NullPointer.into();
1972 }
1973 let h = unsafe { &*handle };
1974 let _op = match h.guard.try_enter() {
1975 Some(op) => op,
1976 None => return NetError::ShuttingDown.into(),
1977 };
1978 let Some(ch) = (unsafe { c_str_to_string(channel) }) else {
1979 return NetError::InvalidUtf8.into();
1980 };
1981 let name = match InnerChannelName::new(&ch) {
1982 Ok(n) => n,
1983 Err(_) => return NET_ERR_CHANNEL,
1984 };
1985 let cfg_in: PublishConfigInput = if config_json.is_null() {
1986 PublishConfigInput::default()
1987 } else {
1988 let Some(s) = (unsafe { c_str_to_string(config_json) }) else {
1989 return NetError::InvalidUtf8.into();
1990 };
1991 match serde_json::from_str(&s) {
1992 Ok(v) => v,
1993 Err(_) => return NetError::InvalidJson.into(),
1994 }
1995 };
1996 let reliability = match cfg_in.reliability.as_deref() {
1997 None | Some("fire_and_forget") => Reliability::FireAndForget,
1998 Some("reliable") => Reliability::Reliable,
1999 Some(_) => return NET_ERR_CHANNEL,
2000 };
2001 let on_failure = match cfg_in.on_failure.as_deref() {
2002 None | Some("best_effort") => InnerOnFailure::BestEffort,
2003 Some("fail_fast") => InnerOnFailure::FailFast,
2004 Some("collect") => InnerOnFailure::Collect,
2005 Some(_) => return NET_ERR_CHANNEL,
2006 };
2007 let max_inflight = cfg_in.max_inflight.unwrap_or(32) as usize;
2008 let publish_cfg = InnerPublishConfig {
2009 reliability,
2010 on_failure,
2011 max_inflight,
2012 };
2013 let publisher = ChannelPublisher::new(name, publish_cfg);
2014
2015 let bytes = if len == 0 {
2017 Bytes::new()
2018 } else if payload.is_null() {
2019 return NetError::NullPointer.into();
2020 } else if len > isize::MAX as usize {
2021 return NetError::InvalidJson.into();
2023 } else {
2024 Bytes::copy_from_slice(unsafe { std::slice::from_raw_parts(payload, len) })
2025 };
2026
2027 let node = h.inner.clone();
2028 match block_on(async move { node.publish(&publisher, bytes).await }) {
2029 Ok(report) => {
2030 let js = to_publish_report_json(report);
2031 write_json_out(&js, out_json, out_len)
2032 }
2033 Err(e) => adapter_err_to_channel_code(&e),
2034 }
2035}
2036
2037pub struct IdentityHandle {
2051 keypair: ManuallyDrop<Arc<EntityKeypair>>,
2052 cache: ManuallyDrop<Arc<TokenCache>>,
2053 guard: HandleGuard,
2054}
2055
2056fn alloc_bytes(src: &[u8], out_ptr: *mut *mut u8, out_len: *mut usize) -> c_int {
2070 if out_ptr.is_null() || out_len.is_null() {
2071 return NetError::NullPointer.into();
2072 }
2073 let len = src.len();
2074 if len == 0 {
2075 unsafe {
2076 *out_ptr = std::ptr::null_mut();
2077 *out_len = 0;
2078 }
2079 return 0;
2080 }
2081 let layout = match std::alloc::Layout::array::<u8>(len) {
2090 Ok(l) => l,
2091 Err(_) => return NET_ERR_IDENTITY,
2097 };
2098 let ptr = unsafe { std::alloc::alloc(layout) };
2099 if ptr.is_null() {
2100 std::alloc::handle_alloc_error(layout);
2101 }
2102 unsafe {
2103 std::ptr::copy_nonoverlapping(src.as_ptr(), ptr, len);
2104 *out_ptr = ptr;
2105 *out_len = len;
2106 }
2107 0
2108}
2109
2110#[unsafe(no_mangle)]
2125pub unsafe extern "C" fn net_free_bytes(ptr: *mut u8, len: usize) {
2126 if ptr.is_null() || len == 0 {
2127 return;
2128 }
2129 let layout = match std::alloc::Layout::array::<u8>(len) {
2135 Ok(l) => l,
2136 Err(_) => return,
2137 };
2138 unsafe {
2139 std::alloc::dealloc(ptr, layout);
2140 }
2141}
2142
2143fn entity_id_from_bytes(bytes: *const u8, len: usize) -> Option<EntityId> {
2144 if bytes.is_null() || len != 32 {
2145 return None;
2146 }
2147 let slice = unsafe { std::slice::from_raw_parts(bytes, 32) };
2148 let mut arr = [0u8; 32];
2149 arr.copy_from_slice(slice);
2150 Some(EntityId::from_bytes(arr))
2151}
2152
2153fn parse_scope_list(raw: &str) -> Option<TokenScope> {
2154 let values: Vec<String> = serde_json::from_str(raw).ok()?;
2158 let mut acc = TokenScope::NONE;
2159 for s in &values {
2160 acc = acc.union(match s.as_str() {
2161 "publish" => TokenScope::PUBLISH,
2162 "subscribe" => TokenScope::SUBSCRIBE,
2163 "admin" => TokenScope::ADMIN,
2164 "delegate" => TokenScope::DELEGATE,
2165 _ => return None,
2166 });
2167 }
2168 Some(acc)
2169}
2170
2171fn scope_to_strings(scope: TokenScope) -> Vec<&'static str> {
2172 let mut out = Vec::new();
2173 if scope.contains(TokenScope::PUBLISH) {
2174 out.push("publish");
2175 }
2176 if scope.contains(TokenScope::SUBSCRIBE) {
2177 out.push("subscribe");
2178 }
2179 if scope.contains(TokenScope::ADMIN) {
2180 out.push("admin");
2181 }
2182 if scope.contains(TokenScope::DELEGATE) {
2183 out.push("delegate");
2184 }
2185 out
2186}
2187
2188fn channel_name_to_hash(channel: &str) -> Option<ChannelHash> {
2189 InnerChannelName::new(channel).ok().map(|n| n.hash())
2190}
2191
2192#[unsafe(no_mangle)]
2195pub unsafe extern "C" fn net_identity_generate(out_handle: *mut *mut IdentityHandle) -> c_int {
2196 if out_handle.is_null() {
2197 return NetError::NullPointer.into();
2198 }
2199 let handle = Box::new(IdentityHandle {
2200 keypair: ManuallyDrop::new(Arc::new(EntityKeypair::generate())),
2201 cache: ManuallyDrop::new(Arc::new(TokenCache::new())),
2202 guard: HandleGuard::new(),
2203 });
2204 unsafe {
2205 *out_handle = Box::into_raw(handle);
2206 }
2207 0
2208}
2209
2210#[unsafe(no_mangle)]
2214pub unsafe extern "C" fn net_identity_from_seed(
2215 seed: *const u8,
2216 seed_len: usize,
2217 out_handle: *mut *mut IdentityHandle,
2218) -> c_int {
2219 if seed.is_null() || out_handle.is_null() {
2220 return NetError::NullPointer.into();
2221 }
2222 if seed_len != 32 {
2223 return NET_ERR_IDENTITY;
2224 }
2225 let mut arr = [0u8; 32];
2226 arr.copy_from_slice(unsafe { std::slice::from_raw_parts(seed, 32) });
2227 let handle = Box::new(IdentityHandle {
2228 keypair: ManuallyDrop::new(Arc::new(EntityKeypair::from_bytes(arr))),
2229 cache: ManuallyDrop::new(Arc::new(TokenCache::new())),
2230 guard: HandleGuard::new(),
2231 });
2232 unsafe {
2233 *out_handle = Box::into_raw(handle);
2234 }
2235 0
2236}
2237
2238#[unsafe(no_mangle)]
2239pub unsafe extern "C" fn net_identity_free(handle: *mut IdentityHandle) {
2240 if handle.is_null() {
2241 return;
2242 }
2243 let h: &IdentityHandle = unsafe { &*handle };
2245 if h.guard.begin_free(FFI_HANDLE_FREE_DEADLINE) {
2246 unsafe {
2248 let mh = &mut *handle;
2249 let kp = ManuallyDrop::take(&mut mh.keypair);
2250 let cache = ManuallyDrop::take(&mut mh.cache);
2251 drop(kp);
2252 drop(cache);
2253 }
2254 } else {
2255 tracing::warn!(
2256 "net_identity_free: in-flight ops did not drain within deadline; \
2257 leaking inner to avoid use-after-free"
2258 );
2259 }
2260}
2261
2262#[unsafe(no_mangle)]
2265pub unsafe extern "C" fn net_identity_to_seed(handle: *mut IdentityHandle, out: *mut u8) -> c_int {
2266 if handle.is_null() || out.is_null() {
2267 return NetError::NullPointer.into();
2268 }
2269 let h = unsafe { &*handle };
2270 let _op = match h.guard.try_enter() {
2271 Some(op) => op,
2272 None => return NetError::ShuttingDown.into(),
2273 };
2274 let seed = h.keypair.secret_bytes();
2275 unsafe {
2276 std::ptr::copy_nonoverlapping(seed.as_ptr(), out, 32);
2277 }
2278 0
2279}
2280
2281#[unsafe(no_mangle)]
2283pub unsafe extern "C" fn net_identity_entity_id(
2284 handle: *mut IdentityHandle,
2285 out: *mut u8,
2286) -> c_int {
2287 if handle.is_null() || out.is_null() {
2288 return NetError::NullPointer.into();
2289 }
2290 let h = unsafe { &*handle };
2291 let _op = match h.guard.try_enter() {
2292 Some(op) => op,
2293 None => return NetError::ShuttingDown.into(),
2294 };
2295 let id = h.keypair.entity_id().as_bytes();
2296 unsafe {
2297 std::ptr::copy_nonoverlapping(id.as_ptr(), out, 32);
2298 }
2299 0
2300}
2301
2302#[unsafe(no_mangle)]
2303pub unsafe extern "C" fn net_identity_node_id(handle: *mut IdentityHandle) -> u64 {
2304 if handle.is_null() {
2305 return 0;
2306 }
2307 let h = unsafe { &*handle };
2308 let _op = match h.guard.try_enter() {
2310 Some(op) => op,
2311 None => return 0,
2312 };
2313 h.keypair.node_id()
2314}
2315
2316#[unsafe(no_mangle)]
2317pub unsafe extern "C" fn net_identity_origin_hash(handle: *mut IdentityHandle) -> u64 {
2318 if handle.is_null() {
2319 return 0;
2320 }
2321 let h = unsafe { &*handle };
2322 let _op = match h.guard.try_enter() {
2324 Some(op) => op,
2325 None => return 0,
2326 };
2327 h.keypair.origin_hash()
2328}
2329
2330#[unsafe(no_mangle)]
2333pub unsafe extern "C" fn net_identity_sign(
2334 handle: *mut IdentityHandle,
2335 msg: *const u8,
2336 len: usize,
2337 out_sig: *mut u8,
2338) -> c_int {
2339 if handle.is_null() || out_sig.is_null() {
2340 return NetError::NullPointer.into();
2341 }
2342 if len > 0 && msg.is_null() {
2343 return NetError::NullPointer.into();
2344 }
2345 let h = unsafe { &*handle };
2346 let _op = match h.guard.try_enter() {
2347 Some(op) => op,
2348 None => return NetError::ShuttingDown.into(),
2349 };
2350 let slice = if len == 0 {
2351 &[][..]
2352 } else if len > isize::MAX as usize {
2353 return NetError::InvalidJson.into();
2355 } else {
2356 unsafe { std::slice::from_raw_parts(msg, len) }
2357 };
2358 let sig = h.keypair.sign(slice).to_bytes();
2359 unsafe {
2360 std::ptr::copy_nonoverlapping(sig.as_ptr(), out_sig, 64);
2361 }
2362 0
2363}
2364
2365#[unsafe(no_mangle)]
2368pub unsafe extern "C" fn net_identity_issue_token(
2369 signer: *mut IdentityHandle,
2370 subject: *const u8,
2371 subject_len: usize,
2372 scope_json: *const c_char,
2373 channel: *const c_char,
2374 ttl_seconds: u32,
2375 delegation_depth: u8,
2376 out_token: *mut *mut u8,
2377 out_token_len: *mut usize,
2378) -> c_int {
2379 if signer.is_null() || out_token.is_null() || out_token_len.is_null() {
2380 return NetError::NullPointer.into();
2381 }
2382 let Some(subject_id) = entity_id_from_bytes(subject, subject_len) else {
2383 return NET_ERR_IDENTITY;
2384 };
2385 let Some(scope_s) = (unsafe { c_str_to_string(scope_json) }) else {
2386 return NetError::InvalidUtf8.into();
2387 };
2388 let Some(scope) = parse_scope_list(&scope_s) else {
2389 return NET_ERR_IDENTITY;
2390 };
2391 let Some(channel_s) = (unsafe { c_str_to_string(channel) }) else {
2392 return NetError::InvalidUtf8.into();
2393 };
2394 let Some(channel_hash) = channel_name_to_hash(&channel_s) else {
2395 return NET_ERR_IDENTITY;
2396 };
2397 let h = unsafe { &*signer };
2398 let _op = match h.guard.try_enter() {
2402 Some(op) => op,
2403 None => return NetError::ShuttingDown.into(),
2404 };
2405 let token = match PermissionToken::try_issue(
2411 &h.keypair,
2412 subject_id,
2413 scope,
2414 channel_hash,
2415 u64::from(ttl_seconds),
2416 delegation_depth,
2417 ) {
2418 Ok(t) => t,
2419 Err(e) => return token_err_to_code(&e),
2420 };
2421 alloc_bytes(&token.to_bytes(), out_token, out_token_len)
2422}
2423
2424#[unsafe(no_mangle)]
2428pub unsafe extern "C" fn net_identity_install_token(
2429 handle: *mut IdentityHandle,
2430 token: *const u8,
2431 len: usize,
2432) -> c_int {
2433 if handle.is_null() || token.is_null() {
2434 return NetError::NullPointer.into();
2435 }
2436 if len > isize::MAX as usize {
2438 return NetError::InvalidJson.into();
2439 }
2440 let slice = unsafe { std::slice::from_raw_parts(token, len) };
2441 let parsed = match PermissionToken::from_bytes(slice) {
2442 Ok(t) => t,
2443 Err(e) => return token_err_to_code(&e),
2444 };
2445 let h = unsafe { &*handle };
2446 let _op = match h.guard.try_enter() {
2447 Some(op) => op,
2448 None => return NetError::ShuttingDown.into(),
2449 };
2450 match h.cache.insert(parsed) {
2451 Ok(()) => 0,
2452 Err(e) => token_err_to_code(&e),
2453 }
2454}
2455
2456#[unsafe(no_mangle)]
2460pub unsafe extern "C" fn net_identity_lookup_token(
2461 handle: *mut IdentityHandle,
2462 subject: *const u8,
2463 subject_len: usize,
2464 channel: *const c_char,
2465 out_token: *mut *mut u8,
2466 out_token_len: *mut usize,
2467) -> c_int {
2468 if handle.is_null() || out_token.is_null() || out_token_len.is_null() {
2469 return NetError::NullPointer.into();
2470 }
2471 let Some(subject_id) = entity_id_from_bytes(subject, subject_len) else {
2472 return NET_ERR_IDENTITY;
2473 };
2474 let Some(channel_s) = (unsafe { c_str_to_string(channel) }) else {
2475 return NetError::InvalidUtf8.into();
2476 };
2477 let Some(channel_hash) = channel_name_to_hash(&channel_s) else {
2478 return NET_ERR_IDENTITY;
2479 };
2480 let h = unsafe { &*handle };
2481 let _op = match h.guard.try_enter() {
2482 Some(op) => op,
2483 None => return NetError::ShuttingDown.into(),
2484 };
2485 match h.cache.get(&subject_id, channel_hash) {
2486 Some(token) => alloc_bytes(&token.to_bytes(), out_token, out_token_len),
2487 None => {
2488 unsafe {
2489 *out_token = std::ptr::null_mut();
2490 *out_token_len = 0;
2491 }
2492 0
2493 }
2494 }
2495}
2496
2497#[unsafe(no_mangle)]
2498pub unsafe extern "C" fn net_identity_token_cache_len(handle: *mut IdentityHandle) -> u32 {
2499 if handle.is_null() {
2500 return 0;
2501 }
2502 let h = unsafe { &*handle };
2503 let _op = match h.guard.try_enter() {
2505 Some(op) => op,
2506 None => return 0,
2507 };
2508 h.cache.len() as u32
2509}
2510
2511#[derive(Serialize)]
2516struct ParsedTokenJson {
2517 issuer_hex: String,
2518 subject_hex: String,
2519 scope: Vec<&'static str>,
2520 channel_hash: ChannelHash,
2521 not_before: u64,
2522 not_after: u64,
2523 delegation_depth: u8,
2524 nonce: u64,
2525 signature_hex: String,
2526}
2527
2528#[unsafe(no_mangle)]
2533pub unsafe extern "C" fn net_parse_token(
2534 token: *const u8,
2535 len: usize,
2536 out_json: *mut *mut c_char,
2537 out_len: *mut usize,
2538) -> c_int {
2539 if token.is_null() || out_json.is_null() || out_len.is_null() {
2540 return NetError::NullPointer.into();
2541 }
2542 if len > isize::MAX as usize {
2544 return NetError::InvalidJson.into();
2545 }
2546 let slice = unsafe { std::slice::from_raw_parts(token, len) };
2547 let parsed = match PermissionToken::from_bytes(slice) {
2548 Ok(t) => t,
2549 Err(e) => return token_err_to_code(&e),
2550 };
2551 let out = ParsedTokenJson {
2552 issuer_hex: hex::encode(parsed.issuer.as_bytes()),
2553 subject_hex: hex::encode(parsed.subject.as_bytes()),
2554 scope: scope_to_strings(parsed.scope),
2555 channel_hash: parsed.channel_hash,
2556 not_before: parsed.not_before,
2557 not_after: parsed.not_after,
2558 delegation_depth: parsed.delegation_depth,
2559 nonce: parsed.nonce,
2560 signature_hex: hex::encode(parsed.signature),
2561 };
2562 write_json_out(&out, out_json, out_len)
2563}
2564
2565#[unsafe(no_mangle)]
2569pub unsafe extern "C" fn net_verify_token(
2570 token: *const u8,
2571 len: usize,
2572 out_ok: *mut c_int,
2573) -> c_int {
2574 if token.is_null() || out_ok.is_null() {
2575 return NetError::NullPointer.into();
2576 }
2577 if len > isize::MAX as usize {
2579 return NetError::InvalidJson.into();
2580 }
2581 let slice = unsafe { std::slice::from_raw_parts(token, len) };
2582 let parsed = match PermissionToken::from_bytes(slice) {
2583 Ok(t) => t,
2584 Err(e) => return token_err_to_code(&e),
2585 };
2586 unsafe {
2587 *out_ok = if parsed.verify().is_ok() { 1 } else { 0 };
2588 }
2589 0
2590}
2591
2592#[unsafe(no_mangle)]
2597pub unsafe extern "C" fn net_token_is_expired(
2598 token: *const u8,
2599 len: usize,
2600 out_expired: *mut c_int,
2601) -> c_int {
2602 if token.is_null() || out_expired.is_null() {
2603 return NetError::NullPointer.into();
2604 }
2605 if len > isize::MAX as usize {
2607 return NetError::InvalidJson.into();
2608 }
2609 let slice = unsafe { std::slice::from_raw_parts(token, len) };
2610 let parsed = match PermissionToken::from_bytes(slice) {
2611 Ok(t) => t,
2612 Err(e) => return token_err_to_code(&e),
2613 };
2614 unsafe {
2615 *out_expired = if parsed.is_expired() { 1 } else { 0 };
2616 }
2617 0
2618}
2619
2620#[unsafe(no_mangle)]
2623pub unsafe extern "C" fn net_delegate_token(
2624 signer: *mut IdentityHandle,
2625 parent: *const u8,
2626 parent_len: usize,
2627 new_subject: *const u8,
2628 new_subject_len: usize,
2629 restricted_scope_json: *const c_char,
2630 out_token: *mut *mut u8,
2631 out_token_len: *mut usize,
2632) -> c_int {
2633 if signer.is_null()
2634 || parent.is_null()
2635 || new_subject.is_null()
2636 || restricted_scope_json.is_null()
2637 || out_token.is_null()
2638 || out_token_len.is_null()
2639 {
2640 return NetError::NullPointer.into();
2641 }
2642 if parent_len > isize::MAX as usize {
2644 return NetError::InvalidJson.into();
2645 }
2646 let parent_slice = unsafe { std::slice::from_raw_parts(parent, parent_len) };
2647 let parent_tok = match PermissionToken::from_bytes(parent_slice) {
2648 Ok(t) => t,
2649 Err(e) => return token_err_to_code(&e),
2650 };
2651 let Some(subject_id) = entity_id_from_bytes(new_subject, new_subject_len) else {
2652 return NET_ERR_IDENTITY;
2653 };
2654 let Some(scope_s) = (unsafe { c_str_to_string(restricted_scope_json) }) else {
2655 return NetError::InvalidUtf8.into();
2656 };
2657 let Some(scope) = parse_scope_list(&scope_s) else {
2658 return NET_ERR_IDENTITY;
2659 };
2660 let h = unsafe { &*signer };
2661 let _op = match h.guard.try_enter() {
2665 Some(op) => op,
2666 None => return NetError::ShuttingDown.into(),
2667 };
2668 match parent_tok.delegate(&h.keypair, subject_id, scope) {
2669 Ok(child) => alloc_bytes(&child.to_bytes(), out_token, out_token_len),
2670 Err(e) => token_err_to_code(&e),
2671 }
2672}
2673
2674#[unsafe(no_mangle)]
2679pub unsafe extern "C" fn net_channel_hash(channel: *const c_char, out_hash: *mut u64) -> c_int {
2680 if channel.is_null() || out_hash.is_null() {
2681 return NetError::NullPointer.into();
2682 }
2683 let Some(s) = (unsafe { c_str_to_string(channel) }) else {
2684 return NetError::InvalidUtf8.into();
2685 };
2686 let Some(hash) = channel_name_to_hash(&s) else {
2687 return NET_ERR_IDENTITY;
2688 };
2689 unsafe {
2690 *out_hash = hash;
2691 }
2692 0
2693}
2694
2695use crate::adapter::net::behavior::capability::{
2702 AcceleratorInfo, AcceleratorType, CapabilityFilter, CapabilitySet, GpuInfo, GpuVendor,
2703 HardwareCapabilities, Modality, ModelCapability, ResourceLimits, SoftwareCapabilities,
2704 ToolCapability, TAG_SCOPE_REGION_PREFIX, TAG_SCOPE_SUBNET_LOCAL, TAG_SCOPE_TENANT_PREFIX,
2705};
2706
2707fn parse_gpu_vendor_cap(s: &str) -> GpuVendor {
2710 match s.to_ascii_lowercase().as_str() {
2711 "nvidia" => GpuVendor::Nvidia,
2712 "amd" => GpuVendor::Amd,
2713 "intel" => GpuVendor::Intel,
2714 "apple" => GpuVendor::Apple,
2715 "qualcomm" => GpuVendor::Qualcomm,
2716 _ => GpuVendor::Unknown,
2717 }
2718}
2719
2720fn gpu_vendor_to_string_cap(v: GpuVendor) -> &'static str {
2721 match v {
2722 GpuVendor::Nvidia => "nvidia",
2723 GpuVendor::Amd => "amd",
2724 GpuVendor::Intel => "intel",
2725 GpuVendor::Apple => "apple",
2726 GpuVendor::Qualcomm => "qualcomm",
2727 GpuVendor::Unknown => "unknown",
2728 }
2729}
2730
2731fn parse_modality_cap(s: &str) -> Option<Modality> {
2732 match s.to_ascii_lowercase().as_str() {
2733 "text" => Some(Modality::Text),
2734 "image" => Some(Modality::Image),
2735 "audio" => Some(Modality::Audio),
2736 "video" => Some(Modality::Video),
2737 "code" => Some(Modality::Code),
2738 "embedding" => Some(Modality::Embedding),
2739 "tool-use" | "tool_use" | "tooluse" => Some(Modality::ToolUse),
2740 _ => None,
2749 }
2750}
2751
2752fn parse_accelerator_type_cap(s: &str) -> AcceleratorType {
2753 match s.to_ascii_lowercase().as_str() {
2754 "tpu" => AcceleratorType::Tpu,
2755 "npu" => AcceleratorType::Npu,
2756 "fpga" => AcceleratorType::Fpga,
2757 "asic" => AcceleratorType::Asic,
2758 "dsp" => AcceleratorType::Dsp,
2759 _ => AcceleratorType::Unknown,
2760 }
2761}
2762
2763#[derive(Deserialize, Default)]
2766struct CapabilitySetJson {
2767 #[serde(default)]
2768 hardware: Option<HardwareJson>,
2769 #[serde(default)]
2770 software: Option<SoftwareJson>,
2771 #[serde(default)]
2772 models: Vec<ModelJson>,
2773 #[serde(default)]
2774 tools: Vec<ToolJson>,
2775 #[serde(default)]
2776 tags: Vec<String>,
2777 #[serde(default)]
2778 limits: Option<LimitsJson>,
2779}
2780
2781#[derive(Deserialize, Default)]
2782struct HardwareJson {
2783 cpu_cores: Option<u32>,
2784 cpu_threads: Option<u32>,
2785 memory_gb: Option<u32>,
2786 gpu: Option<GpuJson>,
2787 #[serde(default)]
2788 additional_gpus: Vec<GpuJson>,
2789 storage_gb: Option<u64>,
2790 network_gbps: Option<u32>,
2791 #[serde(default)]
2792 accelerators: Vec<AcceleratorJson>,
2793}
2794
2795#[derive(Deserialize)]
2796struct GpuJson {
2797 vendor: Option<String>,
2798 #[serde(default)]
2799 model: String,
2800 #[serde(default)]
2801 vram_gb: u32,
2802 compute_units: Option<u32>,
2803 tensor_cores: Option<u32>,
2804 fp16_tflops_x10: Option<u32>,
2805}
2806
2807#[derive(Deserialize)]
2808struct AcceleratorJson {
2809 #[serde(default)]
2810 kind: String,
2811 #[serde(default)]
2812 model: String,
2813 memory_gb: Option<u32>,
2814 tops_x10: Option<u32>,
2815}
2816
2817#[derive(Deserialize, Default)]
2818struct SoftwareJson {
2819 os: Option<String>,
2820 os_version: Option<String>,
2821 #[serde(default)]
2822 runtimes: Vec<Vec<String>>,
2823 #[serde(default)]
2824 frameworks: Vec<Vec<String>>,
2825 cuda_version: Option<String>,
2826 #[serde(default)]
2827 drivers: Vec<Vec<String>>,
2828}
2829
2830#[derive(Deserialize)]
2831struct ModelJson {
2832 #[serde(default)]
2833 model_id: String,
2834 #[serde(default)]
2835 family: String,
2836 parameters_b_x10: Option<u32>,
2837 context_length: Option<u32>,
2838 quantization: Option<String>,
2839 #[serde(default)]
2840 modalities: Vec<String>,
2841 tokens_per_sec: Option<u32>,
2842 loaded: Option<bool>,
2843}
2844
2845#[derive(Deserialize)]
2846struct ToolJson {
2847 #[serde(default)]
2848 tool_id: String,
2849 #[serde(default)]
2850 name: String,
2851 version: Option<String>,
2852 input_schema: Option<String>,
2853 output_schema: Option<String>,
2854 #[serde(default)]
2855 requires: Vec<String>,
2856 estimated_time_ms: Option<u32>,
2857 stateless: Option<bool>,
2858}
2859
2860#[derive(Deserialize, Default)]
2861struct LimitsJson {
2862 max_concurrent_requests: Option<u32>,
2863 max_tokens_per_request: Option<u32>,
2864 rate_limit_rpm: Option<u32>,
2865 max_batch_size: Option<u32>,
2866 max_input_bytes: Option<u32>,
2867 max_output_bytes: Option<u32>,
2868}
2869
2870#[derive(Deserialize, Default)]
2871struct CapabilityFilterJson {
2872 #[serde(default)]
2873 require_tags: Vec<String>,
2874 #[serde(default)]
2875 require_models: Vec<String>,
2876 #[serde(default)]
2877 require_tools: Vec<String>,
2878 min_memory_gb: Option<u32>,
2879 require_gpu: Option<bool>,
2880 gpu_vendor: Option<String>,
2881 min_vram_gb: Option<u32>,
2882 min_context_length: Option<u32>,
2883 #[serde(default)]
2884 require_modalities: Vec<String>,
2885}
2886
2887fn pair_vec(xs: Vec<Vec<String>>) -> Vec<(String, String)> {
2890 xs.into_iter()
2891 .filter_map(|mut p| {
2892 if p.len() >= 2 {
2893 Some((std::mem::take(&mut p[0]), std::mem::take(&mut p[1])))
2894 } else {
2895 None
2896 }
2897 })
2898 .collect()
2899}
2900
2901#[inline]
2907fn saturating_u16_cap(v: u32) -> u16 {
2908 v.min(u16::MAX as u32) as u16
2909}
2910
2911fn gpu_info_from_json(g: GpuJson) -> GpuInfo {
2912 let vendor = g
2913 .vendor
2914 .as_deref()
2915 .map(parse_gpu_vendor_cap)
2916 .unwrap_or(GpuVendor::Unknown);
2917 let mut info = GpuInfo::new(vendor, g.model, g.vram_gb);
2918 if let Some(cu) = g.compute_units {
2919 info = info.with_compute_units(saturating_u16_cap(cu));
2920 }
2921 if let Some(tc) = g.tensor_cores {
2922 info = info.with_tensor_cores(saturating_u16_cap(tc));
2923 }
2924 if let Some(tf) = g.fp16_tflops_x10 {
2925 let tf_capped = saturating_u16_cap(tf);
2939 info = info.with_fp16_tflops(tf_capped as f32 / 10.0);
2940 }
2941 info
2942}
2943
2944fn accelerator_from_json(a: AcceleratorJson) -> AcceleratorInfo {
2945 AcceleratorInfo {
2946 accel_type: parse_accelerator_type_cap(&a.kind),
2947 model: a.model,
2948 memory_gb: a.memory_gb.unwrap_or(0),
2949 tops_x10: a.tops_x10.map(saturating_u16_cap).unwrap_or(0),
2950 }
2951}
2952
2953fn hardware_from_json(h: HardwareJson) -> HardwareCapabilities {
2954 let mut hw = HardwareCapabilities::new();
2955 match (h.cpu_cores, h.cpu_threads) {
2956 (Some(c), Some(t)) => hw = hw.with_cpu(saturating_u16_cap(c), saturating_u16_cap(t)),
2957 (Some(c), None) => {
2958 let c16 = saturating_u16_cap(c);
2959 hw = hw.with_cpu(c16, c16);
2960 }
2961 _ => {}
2962 }
2963 if let Some(mb) = h.memory_gb {
2964 hw = hw.with_memory(mb);
2965 }
2966 if let Some(g) = h.gpu {
2967 hw = hw.with_gpu(gpu_info_from_json(g));
2968 }
2969 for g in h.additional_gpus {
2970 hw = hw.add_gpu(gpu_info_from_json(g));
2971 }
2972 if let Some(mb) = h.storage_gb {
2973 hw = hw.with_storage(mb);
2974 }
2975 if let Some(gbps) = h.network_gbps {
2976 hw = hw.with_network(gbps);
2977 }
2978 for a in h.accelerators {
2979 hw = hw.add_accelerator(accelerator_from_json(a));
2980 }
2981 hw
2982}
2983
2984fn software_from_json(s: SoftwareJson) -> SoftwareCapabilities {
2985 let mut sw = SoftwareCapabilities::new()
2986 .with_os(s.os.unwrap_or_default(), s.os_version.unwrap_or_default());
2987 for (k, v) in pair_vec(s.runtimes) {
2988 sw = sw.add_runtime(k, v);
2989 }
2990 for (k, v) in pair_vec(s.frameworks) {
2991 sw = sw.add_framework(k, v);
2992 }
2993 if let Some(c) = s.cuda_version {
2994 sw = sw.with_cuda(c);
2995 }
2996 sw.drivers = pair_vec(s.drivers);
2997 sw
2998}
2999
3000fn model_from_json(m: ModelJson) -> ModelCapability {
3001 let mut mc = ModelCapability::new(m.model_id, m.family);
3002 if let Some(p) = m.parameters_b_x10 {
3003 mc.parameters_b_x10 = p;
3004 }
3005 if let Some(c) = m.context_length {
3006 mc = mc.with_context_length(c);
3007 }
3008 if let Some(q) = m.quantization {
3009 mc = mc.with_quantization(q);
3010 }
3011 for modality in m.modalities {
3012 match parse_modality_cap(&modality) {
3013 Some(parsed) => mc = mc.add_modality(parsed),
3014 None => {
3015 tracing::warn!(
3016 modality = %modality,
3017 "announce_capabilities: unknown modality string (typo?), \
3018 skipping rather than the pre-fix silent fallback to Text — \
3019 advertising a Text capability the node doesn't actually \
3020 have produced wrong scheduling decisions on the receiver",
3021 );
3022 }
3023 }
3024 }
3025 if let Some(t) = m.tokens_per_sec {
3026 mc = mc.with_tokens_per_sec(t);
3027 }
3028 if let Some(l) = m.loaded {
3029 mc = mc.with_loaded(l);
3030 }
3031 mc
3032}
3033
3034fn tool_from_json(t: ToolJson) -> ToolCapability {
3035 let mut tc = ToolCapability::new(t.tool_id, t.name);
3036 if let Some(v) = t.version {
3037 tc = tc.with_version(v);
3038 }
3039 if let Some(s) = t.input_schema {
3040 tc = tc.with_input_schema(s);
3041 }
3042 if let Some(s) = t.output_schema {
3043 tc = tc.with_output_schema(s);
3044 }
3045 for r in t.requires {
3046 tc = tc.requires(r);
3047 }
3048 if let Some(ms) = t.estimated_time_ms {
3049 tc = tc.with_estimated_time(ms);
3050 }
3051 if let Some(st) = t.stateless {
3052 tc = tc.with_stateless(st);
3053 }
3054 tc
3055}
3056
3057fn limits_from_json(l: LimitsJson) -> ResourceLimits {
3058 let mut rl = ResourceLimits::new();
3059 if let Some(n) = l.max_concurrent_requests {
3060 rl = rl.with_max_concurrent(n);
3061 }
3062 if let Some(n) = l.max_tokens_per_request {
3063 rl = rl.with_max_tokens(n);
3064 }
3065 if let Some(n) = l.rate_limit_rpm {
3066 rl = rl.with_rate_limit(n);
3067 }
3068 if let Some(n) = l.max_batch_size {
3069 rl = rl.with_max_batch(n);
3070 }
3071 if let Some(n) = l.max_input_bytes {
3072 rl.max_input_bytes = n;
3073 }
3074 if let Some(n) = l.max_output_bytes {
3075 rl.max_output_bytes = n;
3076 }
3077 rl
3078}
3079
3080fn capability_set_from_json(caps: CapabilitySetJson) -> CapabilitySet {
3081 let mut cs = CapabilitySet::new();
3082 if let Some(h) = caps.hardware {
3083 cs = cs.with_hardware(hardware_from_json(h));
3084 }
3085 if let Some(s) = caps.software {
3086 cs = cs.with_software(software_from_json(s));
3087 }
3088 for m in caps.models {
3089 cs = cs.add_model(model_from_json(m));
3090 }
3091 for t in caps.tools {
3092 cs = cs.add_tool(tool_from_json(t));
3093 }
3094 for tag in caps.tags {
3102 if tag == TAG_SCOPE_SUBNET_LOCAL {
3103 cs = cs.with_subnet_local_scope();
3104 } else if let Some(id) = tag.strip_prefix(TAG_SCOPE_TENANT_PREFIX) {
3105 cs = cs.with_tenant_scope(id);
3106 } else if let Some(name) = tag.strip_prefix(TAG_SCOPE_REGION_PREFIX) {
3107 cs = cs.with_region_scope(name);
3108 } else {
3109 cs = cs.add_tag(tag);
3110 }
3111 }
3112 if let Some(l) = caps.limits {
3113 cs = cs.with_limits(limits_from_json(l));
3114 }
3115 cs
3116}
3117
3118fn capability_filter_from_json(f: CapabilityFilterJson) -> CapabilityFilter {
3119 let mut cf = CapabilityFilter::new();
3120 for t in f.require_tags {
3121 cf = cf.require_tag(t);
3122 }
3123 for m in f.require_models {
3124 cf = cf.require_model(m);
3125 }
3126 for t in f.require_tools {
3127 cf = cf.require_tool(t);
3128 }
3129 if let Some(mb) = f.min_memory_gb {
3130 cf = cf.with_min_memory(mb);
3131 }
3132 if f.require_gpu.unwrap_or(false) {
3133 cf = cf.require_gpu();
3134 }
3135 if let Some(v) = f.gpu_vendor {
3136 cf = cf.with_gpu_vendor(parse_gpu_vendor_cap(&v));
3137 }
3138 if let Some(mb) = f.min_vram_gb {
3139 cf = cf.with_min_vram(mb);
3140 }
3141 if let Some(n) = f.min_context_length {
3142 cf = cf.with_min_context(n);
3143 }
3144 for m in f.require_modalities {
3145 match parse_modality_cap(&m) {
3146 Some(parsed) => cf = cf.require_modality(parsed),
3147 None => {
3148 tracing::warn!(
3161 modality = %m,
3162 "find_nodes: unknown modality string in require_modalities \
3163 filter (typo?), dropping the constraint; the resulting \
3164 filter is too permissive — pre-fix it was silently \
3165 re-interpreted as `require Text`, which returned the \
3166 wrong nodes",
3167 );
3168 }
3169 }
3170 }
3171 cf
3172}
3173
3174pub(crate) const NET_ERR_CAPABILITY: c_int = -128;
3177
3178#[unsafe(no_mangle)]
3185pub unsafe extern "C" fn net_mesh_announce_capabilities(
3186 handle: *mut MeshNodeHandle,
3187 caps_json: *const c_char,
3188) -> c_int {
3189 if handle.is_null() || caps_json.is_null() {
3190 return NetError::NullPointer.into();
3191 }
3192 let h = unsafe { &*handle };
3193 let _op = match h.guard.try_enter() {
3194 Some(op) => op,
3195 None => return NetError::ShuttingDown.into(),
3196 };
3197 let Some(s) = (unsafe { c_str_to_string(caps_json) }) else {
3198 return NetError::InvalidUtf8.into();
3199 };
3200 let parsed: CapabilitySetJson = match serde_json::from_str(&s) {
3201 Ok(v) => v,
3202 Err(_) => return NetError::InvalidJson.into(),
3203 };
3204 let caps = capability_set_from_json(parsed);
3205 let node = h.inner.clone();
3206 match block_on(async move { node.announce_capabilities(caps).await }) {
3207 Ok(()) => 0,
3208 Err(_) => NET_ERR_CAPABILITY,
3209 }
3210}
3211
3212#[unsafe(no_mangle)]
3215pub unsafe extern "C" fn net_mesh_find_nodes(
3216 handle: *mut MeshNodeHandle,
3217 filter_json: *const c_char,
3218 out_json: *mut *mut c_char,
3219 out_len: *mut usize,
3220) -> c_int {
3221 if handle.is_null() || filter_json.is_null() || out_json.is_null() || out_len.is_null() {
3222 return NetError::NullPointer.into();
3223 }
3224 let h = unsafe { &*handle };
3225 let _op = match h.guard.try_enter() {
3226 Some(op) => op,
3227 None => return NetError::ShuttingDown.into(),
3228 };
3229 let Some(s) = (unsafe { c_str_to_string(filter_json) }) else {
3230 return NetError::InvalidUtf8.into();
3231 };
3232 let parsed: CapabilityFilterJson = match serde_json::from_str(&s) {
3233 Ok(v) => v,
3234 Err(_) => return NetError::InvalidJson.into(),
3235 };
3236 let filter = capability_filter_from_json(parsed);
3237 let ids = h.inner.find_nodes_by_filter(&filter);
3238 write_json_out(&ids, out_json, out_len)
3239}
3240
3241#[derive(serde::Deserialize)]
3258struct ScopeFilterJson {
3259 kind: String,
3260 #[serde(default)]
3261 tenant: Option<String>,
3262 #[serde(default)]
3263 tenants: Option<Vec<String>>,
3264 #[serde(default)]
3265 region: Option<String>,
3266 #[serde(default)]
3267 regions: Option<Vec<String>>,
3268}
3269
3270enum ScopeFilterOwned {
3276 Any,
3277 GlobalOnly,
3278 SameSubnet,
3279 Tenant(String),
3280 Tenants(Vec<String>),
3281 Region(String),
3282 Regions(Vec<String>),
3283}
3284
3285fn scope_filter_from_json(f: ScopeFilterJson) -> ScopeFilterOwned {
3286 match f.kind.as_str() {
3287 "any" => ScopeFilterOwned::Any,
3288 "global_only" | "globalOnly" => ScopeFilterOwned::GlobalOnly,
3289 "same_subnet" | "sameSubnet" => ScopeFilterOwned::SameSubnet,
3290 "tenant" => match f.tenant {
3291 Some(t) if !t.is_empty() => ScopeFilterOwned::Tenant(t),
3292 _ => ScopeFilterOwned::Any,
3293 },
3294 "tenants" => match f.tenants {
3295 Some(ts) => {
3301 let cleaned: Vec<String> = ts.into_iter().filter(|t| !t.is_empty()).collect();
3302 if cleaned.is_empty() {
3303 ScopeFilterOwned::Any
3304 } else {
3305 ScopeFilterOwned::Tenants(cleaned)
3306 }
3307 }
3308 None => ScopeFilterOwned::Any,
3309 },
3310 "region" => match f.region {
3311 Some(r) if !r.is_empty() => ScopeFilterOwned::Region(r),
3312 _ => ScopeFilterOwned::Any,
3313 },
3314 "regions" => match f.regions {
3315 Some(rs) => {
3317 let cleaned: Vec<String> = rs.into_iter().filter(|r| !r.is_empty()).collect();
3318 if cleaned.is_empty() {
3319 ScopeFilterOwned::Any
3320 } else {
3321 ScopeFilterOwned::Regions(cleaned)
3322 }
3323 }
3324 None => ScopeFilterOwned::Any,
3325 },
3326 _ => ScopeFilterOwned::Any,
3327 }
3328}
3329
3330fn with_scope_filter<R>(
3335 owned: &ScopeFilterOwned,
3336 f: impl FnOnce(&crate::adapter::net::behavior::capability::ScopeFilter<'_>) -> R,
3337) -> R {
3338 use crate::adapter::net::behavior::capability::ScopeFilter as F;
3339 match owned {
3340 ScopeFilterOwned::Any => f(&F::Any),
3341 ScopeFilterOwned::GlobalOnly => f(&F::GlobalOnly),
3342 ScopeFilterOwned::SameSubnet => f(&F::SameSubnet),
3343 ScopeFilterOwned::Tenant(t) => f(&F::Tenant(t.as_str())),
3344 ScopeFilterOwned::Tenants(ts) => {
3345 let refs: Vec<&str> = ts.iter().map(|s| s.as_str()).collect();
3346 f(&F::Tenants(refs.as_slice()))
3347 }
3348 ScopeFilterOwned::Region(r) => f(&F::Region(r.as_str())),
3349 ScopeFilterOwned::Regions(rs) => {
3350 let refs: Vec<&str> = rs.iter().map(|s| s.as_str()).collect();
3351 f(&F::Regions(refs.as_slice()))
3352 }
3353 }
3354}
3355
3356#[unsafe(no_mangle)]
3379pub unsafe extern "C" fn net_mesh_find_nodes_scoped(
3380 handle: *mut MeshNodeHandle,
3381 filter_json: *const c_char,
3382 scope_json: *const c_char,
3383 out_json: *mut *mut c_char,
3384 out_len: *mut usize,
3385) -> c_int {
3386 if handle.is_null()
3387 || filter_json.is_null()
3388 || scope_json.is_null()
3389 || out_json.is_null()
3390 || out_len.is_null()
3391 {
3392 return NetError::NullPointer.into();
3393 }
3394 let h = unsafe { &*handle };
3395 let _op = match h.guard.try_enter() {
3396 Some(op) => op,
3397 None => return NetError::ShuttingDown.into(),
3398 };
3399 let Some(filter_s) = (unsafe { c_str_to_string(filter_json) }) else {
3400 return NetError::InvalidUtf8.into();
3401 };
3402 let Some(scope_s) = (unsafe { c_str_to_string(scope_json) }) else {
3403 return NetError::InvalidUtf8.into();
3404 };
3405 let parsed_filter: CapabilityFilterJson = match serde_json::from_str(&filter_s) {
3406 Ok(v) => v,
3407 Err(_) => return NetError::InvalidJson.into(),
3408 };
3409 let parsed_scope: ScopeFilterJson = match serde_json::from_str(&scope_s) {
3410 Ok(v) => v,
3411 Err(_) => return NetError::InvalidJson.into(),
3412 };
3413 let filter = capability_filter_from_json(parsed_filter);
3414 let owned = scope_filter_from_json(parsed_scope);
3415 let ids = with_scope_filter(&owned, |sf| {
3416 h.inner.find_nodes_by_filter_scoped(&filter, sf)
3417 });
3418 write_json_out(&ids, out_json, out_len)
3419}
3420
3421#[derive(serde::Deserialize)]
3435struct CapabilityRequirementJson {
3436 #[serde(default)]
3437 filter: CapabilityFilterJson,
3438 #[serde(default)]
3439 prefer_more_memory: f32,
3440 #[serde(default)]
3441 prefer_more_vram: f32,
3442 #[serde(default)]
3443 prefer_faster_inference: f32,
3444 #[serde(default)]
3445 prefer_loaded_models: f32,
3446}
3447
3448fn capability_requirement_from_json(
3449 j: CapabilityRequirementJson,
3450) -> crate::adapter::net::behavior::capability::CapabilityRequirement {
3451 crate::adapter::net::behavior::capability::CapabilityRequirement::from_filter(
3452 capability_filter_from_json(j.filter),
3453 )
3454 .prefer_memory(j.prefer_more_memory)
3455 .prefer_vram(j.prefer_more_vram)
3456 .prefer_speed(j.prefer_faster_inference)
3457 .prefer_loaded(j.prefer_loaded_models)
3458}
3459
3460#[unsafe(no_mangle)]
3470pub unsafe extern "C" fn net_mesh_find_best_node(
3471 handle: *mut MeshNodeHandle,
3472 requirement_json: *const c_char,
3473 out_node_id: *mut u64,
3474 out_has_match: *mut c_int,
3475) -> c_int {
3476 if handle.is_null()
3477 || requirement_json.is_null()
3478 || out_node_id.is_null()
3479 || out_has_match.is_null()
3480 {
3481 return NetError::NullPointer.into();
3482 }
3483 let h = unsafe { &*handle };
3484 let _op = match h.guard.try_enter() {
3485 Some(op) => op,
3486 None => return NetError::ShuttingDown.into(),
3487 };
3488 let Some(s) = (unsafe { c_str_to_string(requirement_json) }) else {
3489 return NetError::InvalidUtf8.into();
3490 };
3491 let parsed: CapabilityRequirementJson = match serde_json::from_str(&s) {
3492 Ok(v) => v,
3493 Err(_) => return NetError::InvalidJson.into(),
3494 };
3495 let req = capability_requirement_from_json(parsed);
3496 match h.inner.find_best_node(&req) {
3497 Some(node_id) => unsafe {
3498 *out_node_id = node_id;
3499 *out_has_match = 1;
3500 },
3501 None => unsafe {
3502 *out_has_match = 0;
3503 },
3504 }
3505 0
3506}
3507
3508#[unsafe(no_mangle)]
3517pub unsafe extern "C" fn net_mesh_find_best_node_scoped(
3518 handle: *mut MeshNodeHandle,
3519 requirement_json: *const c_char,
3520 scope_json: *const c_char,
3521 out_node_id: *mut u64,
3522 out_has_match: *mut c_int,
3523) -> c_int {
3524 if handle.is_null()
3525 || requirement_json.is_null()
3526 || scope_json.is_null()
3527 || out_node_id.is_null()
3528 || out_has_match.is_null()
3529 {
3530 return NetError::NullPointer.into();
3531 }
3532 let h = unsafe { &*handle };
3533 let _op = match h.guard.try_enter() {
3534 Some(op) => op,
3535 None => return NetError::ShuttingDown.into(),
3536 };
3537 let Some(req_s) = (unsafe { c_str_to_string(requirement_json) }) else {
3538 return NetError::InvalidUtf8.into();
3539 };
3540 let Some(scope_s) = (unsafe { c_str_to_string(scope_json) }) else {
3541 return NetError::InvalidUtf8.into();
3542 };
3543 let parsed_req: CapabilityRequirementJson = match serde_json::from_str(&req_s) {
3544 Ok(v) => v,
3545 Err(_) => return NetError::InvalidJson.into(),
3546 };
3547 let parsed_scope: ScopeFilterJson = match serde_json::from_str(&scope_s) {
3548 Ok(v) => v,
3549 Err(_) => return NetError::InvalidJson.into(),
3550 };
3551 let req = capability_requirement_from_json(parsed_req);
3552 let owned = scope_filter_from_json(parsed_scope);
3553 let result = with_scope_filter(&owned, |sf| h.inner.find_best_node_scoped(&req, sf));
3554 match result {
3555 Some(node_id) => unsafe {
3556 *out_node_id = node_id;
3557 *out_has_match = 1;
3558 },
3559 None => unsafe {
3560 *out_has_match = 0;
3561 },
3562 }
3563 0
3564}
3565
3566#[unsafe(no_mangle)]
3568pub unsafe extern "C" fn net_normalize_gpu_vendor(
3569 raw: *const c_char,
3570 out_json: *mut *mut c_char,
3571 out_len: *mut usize,
3572) -> c_int {
3573 if raw.is_null() || out_json.is_null() || out_len.is_null() {
3574 return NetError::NullPointer.into();
3575 }
3576 let Some(s) = (unsafe { c_str_to_string(raw) }) else {
3577 return NetError::InvalidUtf8.into();
3578 };
3579 let canonical = gpu_vendor_to_string_cap(parse_gpu_vendor_cap(&s));
3580 write_string_out(canonical.to_string(), out_json, out_len)
3581}
3582
3583#[cfg(test)]
3584mod tests {
3585 use super::*;
3586
3587 #[test]
3599 fn saturating_u16_cap_clamps_at_u16_max() {
3600 assert_eq!(saturating_u16_cap(0), 0);
3601 assert_eq!(saturating_u16_cap(42), 42);
3602 assert_eq!(saturating_u16_cap(u16::MAX as u32), u16::MAX);
3603 assert_eq!(saturating_u16_cap(u16::MAX as u32 + 1), u16::MAX);
3604 assert_eq!(saturating_u16_cap(u32::MAX), u16::MAX);
3605 }
3606
3607 #[test]
3616 fn parse_modality_cap_returns_none_on_unknown_strings() {
3617 for (s, expected) in [
3619 ("text", Modality::Text),
3620 ("Text", Modality::Text),
3621 ("TEXT", Modality::Text),
3622 ("image", Modality::Image),
3623 ("audio", Modality::Audio),
3624 ("video", Modality::Video),
3625 ("code", Modality::Code),
3626 ("embedding", Modality::Embedding),
3627 ("tool-use", Modality::ToolUse),
3628 ("tool_use", Modality::ToolUse),
3629 ("tooluse", Modality::ToolUse),
3630 ] {
3631 assert_eq!(
3632 parse_modality_cap(s),
3633 Some(expected),
3634 "known modality `{s}` must parse",
3635 );
3636 }
3637
3638 for s in ["audoi", "imageX", "vidoe", "embeding", "garbage", ""] {
3640 assert_eq!(
3641 parse_modality_cap(s),
3642 None,
3643 "unknown modality `{s}` must return None — pre-fix this \
3644 fell back to Modality::Text, advertising a capability \
3645 the node didn't actually have",
3646 );
3647 }
3648 }
3649
3650 #[test]
3660 fn gpu_info_from_json_saturates_fp16_tflops_to_u16_max() {
3661 let g = GpuJson {
3664 vendor: None,
3665 model: "test".to_string(),
3666 vram_gb: 0,
3667 compute_units: None,
3668 tensor_cores: None,
3669 fp16_tflops_x10: Some(1_000_000_000u32),
3670 };
3671 let info = gpu_info_from_json(g);
3672 assert_eq!(
3676 info.fp16_tflops_x10,
3677 u16::MAX as u32,
3678 "fp16_tflops_x10 must saturate at u16::MAX (65535) instead of \
3679 losing precision through the f32 round-trip; got {}",
3680 info.fp16_tflops_x10,
3681 );
3682
3683 let g_small = GpuJson {
3685 vendor: None,
3686 model: "test".to_string(),
3687 vram_gb: 0,
3688 compute_units: None,
3689 tensor_cores: None,
3690 fp16_tflops_x10: Some(425), };
3692 let info_small = gpu_info_from_json(g_small);
3693 assert_eq!(
3694 info_small.fp16_tflops_x10, 425,
3695 "small fp16_tflops_x10 must round-trip exactly"
3696 );
3697 }
3698
3699 #[test]
3712 fn alloc_bytes_round_trip_across_sizes() {
3713 for size in [0usize, 1, 15, 16, 17, 32, 64, 1024, 8192] {
3714 let src: Vec<u8> = (0..size).map(|i| (i as u8).wrapping_mul(37)).collect();
3715 let mut ptr: *mut u8 = std::ptr::null_mut();
3716 let mut len: usize = 0;
3717 let rc = alloc_bytes(&src, &mut ptr as *mut _, &mut len as *mut _);
3718 assert_eq!(rc, 0);
3719 assert_eq!(len, size);
3720 if size == 0 {
3721 assert!(ptr.is_null());
3722 } else {
3723 assert!(!ptr.is_null());
3724 let observed = unsafe { std::slice::from_raw_parts(ptr, len) };
3725 assert_eq!(observed, &src[..]);
3726 }
3727 unsafe { net_free_bytes(ptr, len) };
3730 }
3731 }
3732
3733 #[test]
3734 fn net_free_bytes_null_and_zero_len_are_noops() {
3735 unsafe { net_free_bytes(std::ptr::null_mut(), 0) };
3737 unsafe { net_free_bytes(std::ptr::null_mut(), 42) };
3738 let mut sentinel: u8 = 0;
3741 unsafe { net_free_bytes(&mut sentinel as *mut u8, 0) };
3742 }
3743
3744 #[test]
3756 fn net_free_bytes_does_not_panic_on_oversized_len() {
3757 let mut sentinel: u8 = 0;
3765 let ptr = &mut sentinel as *mut u8;
3766 unsafe { net_free_bytes(ptr, usize::MAX) };
3769 assert_eq!(sentinel, 0, "sentinel must not have been written through");
3772 }
3773
3774 #[test]
3783 fn net_mesh_shutdown_runs_even_with_outstanding_arc_refs() {
3784 let cfg = serde_json::json!({
3785 "bind_addr": "127.0.0.1:0",
3786 "psk_hex": "0".repeat(64),
3787 });
3788 let cfg_c = CString::new(cfg.to_string()).unwrap();
3789 let mut out: *mut MeshNodeHandle = std::ptr::null_mut();
3790 let rc = unsafe { net_mesh_new(cfg_c.as_ptr(), &mut out) };
3791 assert_eq!(rc, 0, "net_mesh_new failed: {rc}");
3792 assert!(!out.is_null());
3793
3794 let inner_clone = {
3797 let h = unsafe { &*out };
3798 Arc::clone(&h.inner)
3799 };
3800 assert!(Arc::strong_count(&inner_clone) >= 2);
3801 assert!(!inner_clone.is_shutdown());
3802
3803 let rc = unsafe { net_mesh_shutdown(out) };
3804 assert_eq!(rc, 0, "net_mesh_shutdown returned {rc}");
3805 assert!(
3806 inner_clone.is_shutdown(),
3807 "shutdown flag must be set even when extra Arc refs are outstanding"
3808 );
3809
3810 drop(inner_clone);
3811 unsafe { net_mesh_free(out) };
3815 }
3816
3817 #[test]
3829 fn handles_match_rejects_stream_node_mismatch() {
3830 fn make_node_handle() -> *mut MeshNodeHandle {
3831 let cfg = serde_json::json!({
3832 "bind_addr": "127.0.0.1:0",
3833 "psk_hex": "0".repeat(64),
3834 });
3835 let cfg_c = CString::new(cfg.to_string()).unwrap();
3836 let mut out: *mut MeshNodeHandle = std::ptr::null_mut();
3837 let rc = unsafe { net_mesh_new(cfg_c.as_ptr(), &mut out) };
3838 assert_eq!(rc, 0);
3839 assert!(!out.is_null());
3840 out
3841 }
3842
3843 let nh_a = make_node_handle();
3844 let nh_b = make_node_handle();
3845
3846 let sh_a = {
3854 let h = unsafe { &*nh_a };
3855 let node_clone: Arc<MeshNode> = Arc::clone(&h.inner);
3856 MeshStreamHandle {
3857 stream: ManuallyDrop::new(CoreStream {
3858 peer_node_id: 0xDEAD,
3859 stream_id: 1,
3860 epoch: 0,
3861 config: StreamConfig::new(),
3862 }),
3863 _node: ManuallyDrop::new(node_clone),
3864 guard: HandleGuard::new(),
3865 }
3866 };
3867
3868 assert!(
3870 handles_match(&sh_a, unsafe { &*nh_a }),
3871 "stream from node_a + node_a handle must match"
3872 );
3873 assert!(
3875 !handles_match(&sh_a, unsafe { &*nh_b }),
3876 "stream from node_a + node_b handle must be rejected (#19)"
3877 );
3878
3879 unsafe {
3888 let mut sh_a = sh_a;
3889 let _ = ManuallyDrop::take(&mut sh_a.stream);
3890 let _ = ManuallyDrop::take(&mut sh_a._node);
3891 }
3892 unsafe { net_mesh_free(nh_a) };
3893 unsafe { net_mesh_free(nh_b) };
3894 }
3895
3896 #[test]
3903 fn net_mesh_free_is_idempotent() {
3904 let cfg = serde_json::json!({
3905 "bind_addr": "127.0.0.1:0",
3906 "psk_hex": "0".repeat(64),
3907 });
3908 let cfg_c = CString::new(cfg.to_string()).unwrap();
3909 let mut nh: *mut MeshNodeHandle = std::ptr::null_mut();
3910 assert_eq!(unsafe { net_mesh_new(cfg_c.as_ptr(), &mut nh) }, 0);
3911 assert!(!nh.is_null());
3912
3913 unsafe { net_mesh_free(nh) };
3914 unsafe { net_mesh_free(nh) };
3918 }
3919
3920 #[test]
3924 fn net_identity_free_is_idempotent() {
3925 let mut h: *mut IdentityHandle = std::ptr::null_mut();
3926 assert_eq!(unsafe { net_identity_generate(&mut h) }, 0);
3927 assert!(!h.is_null());
3928
3929 unsafe { net_identity_free(h) };
3930 unsafe { net_identity_free(h) };
3932 }
3933
3934 #[test]
3946 fn net_mesh_free_waits_for_inflight_op() {
3947 use std::sync::atomic::{AtomicBool, Ordering};
3948 use std::time::{Duration, Instant};
3949
3950 let cfg = serde_json::json!({
3951 "bind_addr": "127.0.0.1:0",
3952 "psk_hex": "0".repeat(64),
3953 });
3954 let cfg_c = CString::new(cfg.to_string()).unwrap();
3955 let mut nh: *mut MeshNodeHandle = std::ptr::null_mut();
3956 assert_eq!(unsafe { net_mesh_new(cfg_c.as_ptr(), &mut nh) }, 0);
3957 assert!(!nh.is_null());
3958
3959 let nh_addr = nh as usize;
3962 let started = Arc::new(AtomicBool::new(false));
3963 let release = Arc::new(AtomicBool::new(false));
3964 let started_w = started.clone();
3965 let release_w = release.clone();
3966
3967 let worker = std::thread::spawn(move || {
3968 let h = unsafe { &*(nh_addr as *mut MeshNodeHandle) };
3969 let op = h.guard.try_enter().expect("entry must succeed pre-free");
3973 started_w.store(true, Ordering::SeqCst);
3974 while !release_w.load(Ordering::SeqCst) {
3975 std::thread::sleep(Duration::from_millis(1));
3976 }
3977 drop(op);
3978 });
3979
3980 while !started.load(Ordering::SeqCst) {
3982 std::thread::yield_now();
3983 }
3984
3985 let release_clone = release.clone();
3988 std::thread::spawn(move || {
3989 std::thread::sleep(Duration::from_millis(50));
3990 release_clone.store(true, Ordering::SeqCst);
3991 });
3992
3993 let t0 = Instant::now();
3995 unsafe { net_mesh_free(nh) };
3996 let elapsed = t0.elapsed();
3997 assert!(
3998 elapsed >= Duration::from_millis(40),
3999 "net_mesh_free returned in {:?} — pre-fix it would have proceeded \
4000 immediately and the worker's subsequent op would UAF",
4001 elapsed,
4002 );
4003 worker.join().unwrap();
4004 }
4005
4006 #[test]
4013 fn net_mesh_stream_stats_returns_shutting_down_after_free() {
4014 let cfg = serde_json::json!({
4015 "bind_addr": "127.0.0.1:0",
4016 "psk_hex": "0".repeat(64),
4017 });
4018 let cfg_c = CString::new(cfg.to_string()).unwrap();
4019 let mut nh: *mut MeshNodeHandle = std::ptr::null_mut();
4020 assert_eq!(unsafe { net_mesh_new(cfg_c.as_ptr(), &mut nh) }, 0);
4021 assert!(!nh.is_null());
4022
4023 unsafe { net_mesh_free(nh) };
4026
4027 let mut out_json: *mut c_char = std::ptr::null_mut();
4028 let mut out_len: usize = 0;
4029 let rc = unsafe { net_mesh_stream_stats(nh, 0xDEAD, 1, &mut out_json, &mut out_len) };
4030 assert_eq!(
4031 rc,
4032 NetError::ShuttingDown as c_int,
4033 "post-free stream_stats must surface ShuttingDown (got {rc})",
4034 );
4035 assert!(
4036 out_json.is_null(),
4037 "no payload may be written after the guard fires",
4038 );
4039 }
4040
4041 #[test]
4046 fn net_identity_issue_token_returns_shutting_down_after_free() {
4047 let mut signer: *mut IdentityHandle = std::ptr::null_mut();
4048 assert_eq!(unsafe { net_identity_generate(&mut signer) }, 0);
4049 assert!(!signer.is_null());
4050 unsafe { net_identity_free(signer) };
4051
4052 let subject = [0u8; 32];
4055 let scope = CString::new("[\"publish\"]").unwrap();
4056 let channel = CString::new("test-channel").unwrap();
4057 let mut out_token: *mut u8 = std::ptr::null_mut();
4058 let mut out_token_len: usize = 0;
4059 let rc = unsafe {
4060 net_identity_issue_token(
4061 signer,
4062 subject.as_ptr(),
4063 subject.len(),
4064 scope.as_ptr(),
4065 channel.as_ptr(),
4066 60,
4067 0,
4068 &mut out_token,
4069 &mut out_token_len,
4070 )
4071 };
4072 assert_eq!(
4073 rc,
4074 NetError::ShuttingDown as c_int,
4075 "post-free issue_token must surface ShuttingDown (got {rc})",
4076 );
4077 assert!(out_token.is_null(), "no token bytes may be allocated");
4078 }
4079
4080 #[test]
4086 fn net_delegate_token_returns_shutting_down_after_free() {
4087 let mut signer: *mut IdentityHandle = std::ptr::null_mut();
4088 assert_eq!(unsafe { net_identity_generate(&mut signer) }, 0);
4089 assert!(!signer.is_null());
4090
4091 let subject = [0u8; 32];
4093 let scope = CString::new("[\"publish\",\"delegate\"]").unwrap();
4094 let channel = CString::new("test-channel").unwrap();
4095 let mut parent_bytes: *mut u8 = std::ptr::null_mut();
4096 let mut parent_len: usize = 0;
4097 assert_eq!(
4098 unsafe {
4099 net_identity_issue_token(
4100 signer,
4101 subject.as_ptr(),
4102 subject.len(),
4103 scope.as_ptr(),
4104 channel.as_ptr(),
4105 60,
4106 1,
4107 &mut parent_bytes,
4108 &mut parent_len,
4109 )
4110 },
4111 0,
4112 );
4113 assert!(!parent_bytes.is_null());
4114
4115 unsafe { net_identity_free(signer) };
4117
4118 let new_subject = [1u8; 32];
4119 let restricted = CString::new("[\"publish\"]").unwrap();
4120 let mut child_bytes: *mut u8 = std::ptr::null_mut();
4121 let mut child_len: usize = 0;
4122 let rc = unsafe {
4123 net_delegate_token(
4124 signer,
4125 parent_bytes,
4126 parent_len,
4127 new_subject.as_ptr(),
4128 new_subject.len(),
4129 restricted.as_ptr(),
4130 &mut child_bytes,
4131 &mut child_len,
4132 )
4133 };
4134 assert_eq!(
4135 rc,
4136 NetError::ShuttingDown as c_int,
4137 "post-free delegate_token must surface ShuttingDown (got {rc})",
4138 );
4139 assert!(child_bytes.is_null(), "no child token may be allocated");
4140
4141 unsafe { net_free_bytes(parent_bytes, parent_len) };
4143 }
4144
4145 #[test]
4146 fn hardware_from_json_saturates_overflow_cpu_fields() {
4147 let h = HardwareJson {
4150 cpu_cores: Some(70_000),
4151 cpu_threads: Some(200_000),
4152 memory_gb: None,
4153 gpu: None,
4154 additional_gpus: Vec::new(),
4155 storage_gb: None,
4156 network_gbps: None,
4157 accelerators: Vec::new(),
4158 };
4159 let hw = hardware_from_json(h);
4160 assert_eq!(hw.cpu_cores, u16::MAX);
4161 assert_eq!(hw.cpu_threads, u16::MAX);
4162 }
4163
4164 #[test]
4171 fn token_entry_points_reject_oversize_len() {
4172 let invalid_json: c_int = NetError::InvalidJson.into();
4173 let mut sentinel: u8 = 0;
4174 let token = &mut sentinel as *mut u8 as *const u8;
4175
4176 let mut out_json: *mut c_char = std::ptr::null_mut();
4177 let mut out_len: usize = 0;
4178 assert_eq!(
4179 unsafe { net_parse_token(token, usize::MAX, &mut out_json, &mut out_len) },
4180 invalid_json,
4181 );
4182 assert!(out_json.is_null());
4183
4184 let mut out_ok: c_int = -42;
4185 assert_eq!(
4186 unsafe { net_verify_token(token, usize::MAX, &mut out_ok) },
4187 invalid_json,
4188 );
4189
4190 let mut out_expired: c_int = -42;
4191 assert_eq!(
4192 unsafe { net_token_is_expired(token, usize::MAX, &mut out_expired) },
4193 invalid_json,
4194 );
4195
4196 assert_eq!(
4197 sentinel, 0,
4198 "sentinel must not be touched: the length guard fires before any deref"
4199 );
4200 }
4201}
4202
4203#[cfg(all(test, not(feature = "nat-traversal")))]
4204mod nat_traversal_stub_tests {
4205 use super::*;
4222 use std::ptr;
4223
4224 #[test]
4225 fn nat_type_stub_returns_unsupported() {
4226 let mut out_str: *mut c_char = ptr::null_mut();
4227 let mut out_len: usize = 0;
4228 let code = unsafe { net_mesh_nat_type(ptr::null_mut(), &mut out_str, &mut out_len) };
4231 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
4232 }
4233
4234 #[test]
4235 fn reflex_addr_stub_returns_unsupported() {
4236 let mut out_str: *mut c_char = ptr::null_mut();
4237 let mut out_len: usize = 0;
4238 let code = unsafe { net_mesh_reflex_addr(ptr::null_mut(), &mut out_str, &mut out_len) };
4240 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
4241 }
4242
4243 #[test]
4244 fn peer_nat_type_stub_returns_unsupported() {
4245 let mut out_str: *mut c_char = ptr::null_mut();
4246 let mut out_len: usize = 0;
4247 let code =
4249 unsafe { net_mesh_peer_nat_type(ptr::null_mut(), 0, &mut out_str, &mut out_len) };
4250 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
4251 }
4252
4253 #[test]
4254 fn probe_reflex_stub_returns_unsupported() {
4255 let mut out_str: *mut c_char = ptr::null_mut();
4256 let mut out_len: usize = 0;
4257 let code = unsafe { net_mesh_probe_reflex(ptr::null_mut(), 0, &mut out_str, &mut out_len) };
4259 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
4260 }
4261
4262 #[test]
4263 fn reclassify_nat_stub_returns_unsupported() {
4264 let code = unsafe { net_mesh_reclassify_nat(ptr::null_mut()) };
4266 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
4267 }
4268
4269 #[test]
4270 fn traversal_stats_stub_returns_unsupported() {
4271 let mut a: u64 = 0;
4272 let mut b: u64 = 0;
4273 let mut c: u64 = 0;
4274 let code = unsafe { net_mesh_traversal_stats(ptr::null_mut(), &mut a, &mut b, &mut c) };
4276 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
4277 }
4278
4279 #[test]
4280 fn connect_direct_stub_returns_unsupported() {
4281 let code = unsafe { net_mesh_connect_direct(ptr::null_mut(), 0, ptr::null(), 0) };
4283 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
4284 }
4285
4286 #[test]
4287 fn set_reflex_override_stub_returns_unsupported() {
4288 let code = unsafe { net_mesh_set_reflex_override(ptr::null_mut(), ptr::null()) };
4290 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
4291 }
4292
4293 #[test]
4294 fn clear_reflex_override_stub_returns_unsupported() {
4295 let code = unsafe { net_mesh_clear_reflex_override(ptr::null_mut()) };
4297 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
4298 }
4299
4300 #[test]
4306 fn unsupported_code_is_stable() {
4307 assert_eq!(NET_ERR_TRAVERSAL_UNSUPPORTED, -137);
4308 }
4309
4310 #[test]
4314 fn capability_set_from_go_marshal_preserves_gpu_vendor() {
4315 let json = r#"{"hardware":{"cpu_cores":16,"memory_gb":64,"gpu":{"vendor":"nvidia","model":"h100","vram_gb":80}},"tags":["gpu"]}"#;
4316 let parsed: CapabilitySetJson = serde_json::from_str(json).expect("JSON should parse");
4317 let caps = capability_set_from_json(parsed);
4318 let views = caps.views();
4322 assert_eq!(
4323 views.hardware().gpu_vendor(),
4324 Some(super::GpuVendor::Nvidia),
4325 "vendor lost in conversion"
4326 );
4327 assert_eq!(views.hardware().memory_gb, 64);
4328 assert_eq!(views.hardware().total_vram_gb(), 80);
4329 assert!(caps.has_tag("gpu"));
4330 }
4331
4332 #[test]
4341 fn collect_payloads_rejects_null_entry_with_nonzero_length() {
4342 let buf_a = b"hello".as_slice();
4343 let buf_b = b"world".as_slice();
4344 let ptrs: [*const u8; 3] = [buf_a.as_ptr(), std::ptr::null(), buf_b.as_ptr()];
4345 let lens: [usize; 3] = [buf_a.len(), 4, buf_b.len()];
4346
4347 let result = unsafe { collect_payloads(ptrs.as_ptr(), lens.as_ptr(), 3) };
4348 assert!(
4349 result.is_none(),
4350 "null entry with non-zero length must reject the whole batch"
4351 );
4352 }
4353
4354 #[test]
4355 fn collect_payloads_allows_null_entry_with_zero_length() {
4356 let buf_a = b"hello".as_slice();
4357 let ptrs: [*const u8; 2] = [buf_a.as_ptr(), std::ptr::null()];
4358 let lens: [usize; 2] = [buf_a.len(), 0];
4359
4360 let result = unsafe { collect_payloads(ptrs.as_ptr(), lens.as_ptr(), 2) }
4361 .expect("zero-length null is treated as empty payload");
4362 assert_eq!(result.len(), 2);
4363 assert_eq!(&result[0][..], b"hello");
4364 assert!(result[1].is_empty());
4365 }
4366
4367 #[test]
4368 fn collect_payloads_happy_path() {
4369 let buf_a = b"abc".as_slice();
4370 let buf_b = b"defg".as_slice();
4371 let ptrs: [*const u8; 2] = [buf_a.as_ptr(), buf_b.as_ptr()];
4372 let lens: [usize; 2] = [buf_a.len(), buf_b.len()];
4373
4374 let result = unsafe { collect_payloads(ptrs.as_ptr(), lens.as_ptr(), 2) }
4375 .expect("non-null entries should succeed");
4376 assert_eq!(result.len(), 2);
4377 assert_eq!(&result[0][..], b"abc");
4378 assert_eq!(&result[1][..], b"defg");
4379 }
4380}