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