1#![allow(clippy::missing_safety_doc)]
44#![expect(
45 clippy::undocumented_unsafe_blocks,
46 reason = "module-wide FFI safety contract documented in the # Safety preamble above"
47)]
48#![expect(
49 clippy::multiple_unsafe_ops_per_block,
50 reason = "FFI entry points routinely deref + write to multiple out-parameter fields under the same caller contract; splitting per-op would obscure the single boundary-cross"
51)]
52
53use std::ffi::{c_char, c_int, CStr, CString};
54use std::mem::ManuallyDrop;
55use std::sync::Arc;
56
57use bytes::Bytes;
58use serde::{Deserialize, Serialize};
59use tokio::runtime::Runtime;
60
61use crate::adapter::net::identity::{
62 EntityId, PermissionToken, TokenCache, TokenError as CoreTokenError, TokenScope,
63};
64use crate::adapter::net::{
65 ChannelConfig as InnerChannelConfig, ChannelConfigRegistry, ChannelHash, ChannelId,
66 ChannelName as InnerChannelName, ChannelPublisher, EntityKeypair, MeshNode, MeshNodeConfig,
67 OnFailure as InnerOnFailure, PublishConfig as InnerPublishConfig,
68 PublishReport as InnerPublishReport, Reliability, Stream as CoreStream, StreamConfig,
69 StreamError, Visibility as InnerVisibility, DEFAULT_STREAM_WINDOW_BYTES,
70};
71use crate::adapter::net::{SubnetId, SubnetPolicy, SubnetRule};
72use crate::adapter::Adapter;
73use crate::error::AdapterError;
74
75use super::handle_guard::{HandleGuard, FFI_HANDLE_FREE_DEADLINE};
76use super::NetError;
77
78pub(crate) const NET_ERR_MESH_INIT: c_int = -110;
84pub(crate) const NET_ERR_MESH_HANDSHAKE: c_int = -111;
85pub(crate) const NET_ERR_MESH_BACKPRESSURE: c_int = -112;
86pub(crate) const NET_ERR_MESH_NOT_CONNECTED: c_int = -113;
87pub(crate) const NET_ERR_MESH_TRANSPORT: c_int = -114;
88pub(crate) const NET_ERR_CHANNEL: c_int = -115;
89pub(crate) const NET_ERR_CHANNEL_AUTH: c_int = -116;
90
91pub(crate) const NET_ERR_IDENTITY: c_int = -120;
96pub(crate) const NET_ERR_TOKEN_INVALID_FORMAT: c_int = -121;
97pub(crate) const NET_ERR_TOKEN_INVALID_SIGNATURE: c_int = -122;
98pub(crate) const NET_ERR_TOKEN_EXPIRED: c_int = -123;
99pub(crate) const NET_ERR_TOKEN_NOT_YET_VALID: c_int = -124;
100pub(crate) const NET_ERR_TOKEN_DELEGATION_EXHAUSTED: c_int = -125;
101pub(crate) const NET_ERR_TOKEN_DELEGATION_NOT_ALLOWED: c_int = -126;
102pub(crate) const NET_ERR_TOKEN_NOT_AUTHORIZED: c_int = -127;
103
104#[cfg(feature = "nat-traversal")]
117pub(crate) const NET_ERR_TRAVERSAL_REFLEX_TIMEOUT: c_int = -130;
118#[cfg(feature = "nat-traversal")]
119pub(crate) const NET_ERR_TRAVERSAL_PEER_NOT_REACHABLE: c_int = -131;
120#[cfg(feature = "nat-traversal")]
121pub(crate) const NET_ERR_TRAVERSAL_TRANSPORT: c_int = -132;
122#[cfg(feature = "nat-traversal")]
123pub(crate) const NET_ERR_TRAVERSAL_RENDEZVOUS_NO_RELAY: c_int = -133;
124#[cfg(feature = "nat-traversal")]
125pub(crate) const NET_ERR_TRAVERSAL_RENDEZVOUS_REJECTED: c_int = -134;
126#[cfg(feature = "nat-traversal")]
127pub(crate) const NET_ERR_TRAVERSAL_PUNCH_FAILED: c_int = -135;
128#[cfg(feature = "nat-traversal")]
129pub(crate) const NET_ERR_TRAVERSAL_PORT_MAP_UNAVAILABLE: c_int = -136;
130pub(crate) const NET_ERR_TRAVERSAL_UNSUPPORTED: c_int = -137;
136
137#[cfg(feature = "nat-traversal")]
138fn traversal_err_to_code(e: &crate::adapter::net::traversal::TraversalError) -> c_int {
139 use crate::adapter::net::traversal::TraversalError;
140 match e {
141 TraversalError::ReflexTimeout => NET_ERR_TRAVERSAL_REFLEX_TIMEOUT,
142 TraversalError::PeerNotReachable => NET_ERR_TRAVERSAL_PEER_NOT_REACHABLE,
143 TraversalError::Transport(_) => NET_ERR_TRAVERSAL_TRANSPORT,
144 TraversalError::RendezvousNoRelay => NET_ERR_TRAVERSAL_RENDEZVOUS_NO_RELAY,
145 TraversalError::RendezvousRejected(_) => NET_ERR_TRAVERSAL_RENDEZVOUS_REJECTED,
146 TraversalError::PunchFailed => NET_ERR_TRAVERSAL_PUNCH_FAILED,
147 TraversalError::PortMapUnavailable => NET_ERR_TRAVERSAL_PORT_MAP_UNAVAILABLE,
148 TraversalError::Unsupported => NET_ERR_TRAVERSAL_UNSUPPORTED,
149 }
150}
151
152#[cfg(feature = "nat-traversal")]
156fn nat_class_to_str(class: crate::adapter::net::traversal::classify::NatClass) -> &'static str {
157 use crate::adapter::net::traversal::classify::NatClass;
158 match class {
159 NatClass::Open => "open",
160 NatClass::Cone => "cone",
161 NatClass::Symmetric => "symmetric",
162 NatClass::Unknown => "unknown",
163 }
164}
165
166fn token_err_to_code(e: &CoreTokenError) -> c_int {
167 match e {
168 CoreTokenError::InvalidFormat => NET_ERR_TOKEN_INVALID_FORMAT,
169 CoreTokenError::InvalidSignature => NET_ERR_TOKEN_INVALID_SIGNATURE,
170 CoreTokenError::Expired => NET_ERR_TOKEN_EXPIRED,
171 CoreTokenError::NotYetValid => NET_ERR_TOKEN_NOT_YET_VALID,
172 CoreTokenError::DelegationExhausted => NET_ERR_TOKEN_DELEGATION_EXHAUSTED,
173 CoreTokenError::DelegationNotAllowed => NET_ERR_TOKEN_DELEGATION_NOT_ALLOWED,
174 CoreTokenError::NotAuthorized => NET_ERR_TOKEN_NOT_AUTHORIZED,
175 CoreTokenError::ReadOnly => NET_ERR_IDENTITY,
180 CoreTokenError::ZeroTtl => NET_ERR_TOKEN_INVALID_FORMAT,
187 CoreTokenError::TtlTooLong => NET_ERR_TOKEN_INVALID_FORMAT,
191 }
192}
193
194fn runtime() -> &'static Arc<Runtime> {
210 use std::sync::OnceLock;
211 static RT: OnceLock<Arc<Runtime>> = OnceLock::new();
212 RT.get_or_init(|| {
213 match tokio::runtime::Builder::new_multi_thread()
214 .enable_all()
215 .build()
216 {
217 Ok(rt) => Arc::new(rt),
218 Err(e) => {
219 eprintln!(
220 "FATAL: mesh FFI tokio runtime build failure ({e:?}); aborting to avoid panic across the FFI boundary"
221 );
222 std::process::abort();
223 }
224 }
225 })
226}
227
228pub(super) fn block_on<F: std::future::Future>(future: F) -> F::Output {
248 if tokio::runtime::Handle::try_current().is_ok() {
249 eprintln!(
250 "FATAL: mesh FFI called from inside a tokio runtime context; \
251 aborting to avoid runtime-in-runtime panic across the FFI boundary"
252 );
253 std::process::abort();
254 }
255 runtime().block_on(future)
256}
257
258#[inline]
281pub(super) unsafe fn c_str_to_string(p: *const c_char) -> Option<String> {
282 if p.is_null() {
283 return None;
284 }
285 CStr::from_ptr(p).to_str().ok().map(str::to_owned)
286}
287
288fn write_json_out<T: Serialize>(
294 value: &T,
295 out_ptr: *mut *mut c_char,
296 out_len: *mut usize,
297) -> c_int {
298 if out_ptr.is_null() || out_len.is_null() {
299 return NetError::NullPointer.into();
300 }
301 let Ok(s) = serde_json::to_string(value) else {
302 return NetError::Unknown.into();
303 };
304 let len = s.len();
305 let Ok(cs) = CString::new(s) else {
306 return NetError::Unknown.into();
307 };
308 unsafe {
309 *out_ptr = cs.into_raw();
310 *out_len = len;
311 }
312 0
313}
314
315pub(super) fn write_string_out(s: String, out_ptr: *mut *mut c_char, out_len: *mut usize) -> c_int {
316 if out_ptr.is_null() || out_len.is_null() {
317 return NetError::NullPointer.into();
318 }
319 let len = s.len();
320 let Ok(cs) = CString::new(s) else {
321 return NetError::Unknown.into();
322 };
323 unsafe {
324 *out_ptr = cs.into_raw();
325 *out_len = len;
326 }
327 0
328}
329
330fn adapter_err_to_code(err: &AdapterError) -> c_int {
331 match err {
332 AdapterError::Connection(_) => NET_ERR_MESH_HANDSHAKE,
333 _ => NET_ERR_MESH_TRANSPORT,
334 }
335}
336
337fn stream_err_to_code(err: &StreamError) -> c_int {
338 match err {
339 StreamError::Backpressure => NET_ERR_MESH_BACKPRESSURE,
340 StreamError::NotConnected => NET_ERR_MESH_NOT_CONNECTED,
341 StreamError::Transport(_) => NET_ERR_MESH_TRANSPORT,
342 }
343}
344
345#[derive(Deserialize)]
350struct SubnetPolicyJson {
351 #[serde(default)]
352 rules: Vec<SubnetRuleJson>,
353}
354
355#[derive(Deserialize)]
356struct SubnetRuleJson {
357 tag_prefix: String,
358 level: u32,
359 #[serde(default)]
360 values: std::collections::HashMap<String, u32>,
361}
362
363fn u8_from_u32(value: u32) -> Option<u8> {
364 if value > 255 {
365 None
366 } else {
367 Some(value as u8)
368 }
369}
370
371fn subnet_id_from_json(levels: Vec<u32>) -> Option<SubnetId> {
372 if levels.is_empty() || levels.len() > 4 {
373 return None;
374 }
375 let mut bytes = [0u8; 4];
376 for (i, raw) in levels.iter().enumerate() {
377 bytes[i] = u8_from_u32(*raw)?;
378 }
379 Some(SubnetId::new(&bytes[..levels.len()]))
380}
381
382fn subnet_policy_from_json(p: SubnetPolicyJson) -> Option<SubnetPolicy> {
383 let mut policy = SubnetPolicy::new();
384 for rule_json in p.rules {
385 let level = u8_from_u32(rule_json.level)?;
386 if level > 3 {
387 return None;
388 }
389 let mut rule = SubnetRule::new(rule_json.tag_prefix, level);
390 for (tag_value, raw_val) in rule_json.values {
391 let v = u8_from_u32(raw_val)?;
392 if v == 0 {
398 return None;
399 }
400 rule = rule.map(tag_value, v);
401 }
402 policy = policy.add_rule(rule);
403 }
404 Some(policy)
405}
406
407#[derive(Deserialize)]
408struct MeshNewConfig {
409 bind_addr: String,
410 psk_hex: String,
412 heartbeat_ms: Option<u64>,
413 session_timeout_ms: Option<u64>,
414 num_shards: Option<u16>,
415 capability_gc_interval_ms: Option<u64>,
418 require_signed_capabilities: Option<bool>,
421 subnet: Option<Vec<u32>>,
423 subnet_policy: Option<SubnetPolicyJson>,
425 identity_seed_hex: Option<String>,
430 #[serde(default)]
436 reflex_override: Option<String>,
437 #[serde(default)]
441 try_port_mapping: bool,
442}
443
444pub struct MeshNodeHandle {
457 inner: ManuallyDrop<Arc<MeshNode>>,
458 channel_configs: ManuallyDrop<Arc<ChannelConfigRegistry>>,
459 guard: HandleGuard,
460}
461
462#[unsafe(no_mangle)]
477pub unsafe extern "C" fn net_mesh_new(
478 config_json: *const c_char,
479 out_handle: *mut *mut MeshNodeHandle,
480) -> c_int {
481 if config_json.is_null() || out_handle.is_null() {
482 return NetError::NullPointer.into();
483 }
484 let Some(s) = (unsafe { c_str_to_string(config_json) }) else {
485 return NetError::InvalidUtf8.into();
486 };
487 let cfg: MeshNewConfig = match serde_json::from_str(&s) {
488 Ok(v) => v,
489 Err(_) => return NetError::InvalidJson.into(),
490 };
491 let bind_addr: std::net::SocketAddr = match cfg.bind_addr.parse() {
492 Ok(a) => a,
493 Err(_) => return NET_ERR_MESH_INIT,
494 };
495 let psk_bytes = match hex::decode(&cfg.psk_hex) {
496 Ok(b) => b,
497 Err(_) => return NET_ERR_MESH_INIT,
498 };
499 if psk_bytes.len() != 32 {
500 return NET_ERR_MESH_INIT;
501 }
502 let mut psk = [0u8; 32];
503 psk.copy_from_slice(&psk_bytes);
504
505 let mut node_cfg = MeshNodeConfig::new(bind_addr, psk);
506 if let Some(ms) = cfg.heartbeat_ms {
514 if ms == 0 {
515 return NetError::InvalidJson.into();
516 }
517 node_cfg = node_cfg.with_heartbeat_interval(std::time::Duration::from_millis(ms));
518 }
519 if let Some(ms) = cfg.session_timeout_ms {
520 if ms == 0 {
521 return NetError::InvalidJson.into();
522 }
523 node_cfg = node_cfg.with_session_timeout(std::time::Duration::from_millis(ms));
524 }
525 if let Some(n) = cfg.num_shards {
526 node_cfg = node_cfg.with_num_shards(n);
527 }
528 if let Some(ms) = cfg.capability_gc_interval_ms {
529 node_cfg = node_cfg.with_capability_gc_interval(std::time::Duration::from_millis(ms));
530 }
531 if let Some(b) = cfg.require_signed_capabilities {
532 node_cfg = node_cfg.with_require_signed_capabilities(b);
533 }
534 if let Some(levels) = cfg.subnet {
535 let Some(id) = subnet_id_from_json(levels) else {
536 return NET_ERR_MESH_INIT;
537 };
538 node_cfg = node_cfg.with_subnet(id);
539 }
540 if let Some(policy_js) = cfg.subnet_policy {
541 let Some(policy) = subnet_policy_from_json(policy_js) else {
542 return NET_ERR_MESH_INIT;
543 };
544 node_cfg = node_cfg.with_subnet_policy(Arc::new(policy));
545 }
546 #[cfg(feature = "nat-traversal")]
547 if let Some(external_str) = cfg.reflex_override.as_deref() {
548 let Ok(external) = external_str.parse::<std::net::SocketAddr>() else {
549 return NET_ERR_MESH_INIT;
550 };
551 node_cfg = node_cfg.with_reflex_override(external);
552 }
553 #[cfg(not(feature = "nat-traversal"))]
557 let _ = cfg.reflex_override;
558 #[cfg(feature = "port-mapping")]
559 if cfg.try_port_mapping {
560 node_cfg = node_cfg.with_try_port_mapping(true);
561 }
562 #[cfg(not(feature = "port-mapping"))]
564 let _ = cfg.try_port_mapping;
565
566 let identity = match cfg.identity_seed_hex {
567 Some(seed_hex) => {
568 let bytes = match hex::decode(&seed_hex) {
569 Ok(b) => b,
570 Err(_) => return NET_ERR_MESH_INIT,
571 };
572 if bytes.len() != 32 {
573 return NET_ERR_MESH_INIT;
574 }
575 let mut arr = [0u8; 32];
576 arr.copy_from_slice(&bytes);
577 EntityKeypair::from_bytes(arr)
578 }
579 None => EntityKeypair::generate(),
580 };
581 let result = block_on(async move { MeshNode::new(identity, node_cfg).await });
582 match result {
583 Ok(mut node) => {
584 let channel_configs = Arc::new(ChannelConfigRegistry::new());
585 node.set_channel_configs(channel_configs.clone());
586 node.set_token_cache(Arc::new(TokenCache::new()));
590 let handle = Box::new(MeshNodeHandle {
591 inner: ManuallyDrop::new(Arc::new(node)),
592 channel_configs: ManuallyDrop::new(channel_configs),
593 guard: HandleGuard::new(),
594 });
595 unsafe {
596 *out_handle = Box::into_raw(handle);
597 }
598 0
599 }
600 Err(_) => NET_ERR_MESH_INIT,
601 }
602}
603
604#[unsafe(no_mangle)]
605pub unsafe extern "C" fn net_mesh_free(handle: *mut MeshNodeHandle) {
606 if handle.is_null() {
607 return;
608 }
609 let h: &MeshNodeHandle = unsafe { &*handle };
614 if h.guard.begin_free(FFI_HANDLE_FREE_DEADLINE) {
615 unsafe {
617 let mh = &mut *handle;
618 let inner = ManuallyDrop::take(&mut mh.inner);
619 let configs = ManuallyDrop::take(&mut mh.channel_configs);
620 drop(inner);
621 drop(configs);
622 }
623 } else {
624 tracing::warn!(
625 "net_mesh_free: in-flight ops did not drain within deadline; \
626 leaking inner to avoid use-after-free"
627 );
628 }
629}
630
631#[cfg(feature = "cortex")]
646pub(super) fn mesh_node_arc(h: &MeshNodeHandle) -> Option<Arc<MeshNode>> {
647 let _op = h.guard.try_enter()?;
648 Some(Arc::clone(&h.inner))
649}
650
651#[unsafe(no_mangle)]
659pub unsafe extern "C" fn net_mesh_arc_clone(handle: *mut MeshNodeHandle) -> *mut Arc<MeshNode> {
660 if handle.is_null() {
661 return std::ptr::null_mut();
662 }
663 let h = unsafe { &*handle };
664 let _op = match h.guard.try_enter() {
666 Some(op) => op,
667 None => return std::ptr::null_mut(),
668 };
669 let cloned: Arc<MeshNode> = Arc::clone(&h.inner);
670 Box::into_raw(Box::new(cloned))
671}
672
673#[unsafe(no_mangle)]
680pub unsafe extern "C" fn net_mesh_channel_configs_arc_clone(
681 handle: *mut MeshNodeHandle,
682) -> *mut Arc<ChannelConfigRegistry> {
683 if handle.is_null() {
684 return std::ptr::null_mut();
685 }
686 let h = unsafe { &*handle };
687 let _op = match h.guard.try_enter() {
689 Some(op) => op,
690 None => return std::ptr::null_mut(),
691 };
692 let cloned: Arc<ChannelConfigRegistry> = Arc::clone(&h.channel_configs);
693 Box::into_raw(Box::new(cloned))
694}
695
696#[unsafe(no_mangle)]
699pub unsafe extern "C" fn net_mesh_arc_free(p: *mut Arc<MeshNode>) {
700 if p.is_null() {
701 return;
702 }
703 unsafe {
704 drop(Box::from_raw(p));
705 }
706}
707
708#[unsafe(no_mangle)]
711pub unsafe extern "C" fn net_mesh_channel_configs_arc_free(p: *mut Arc<ChannelConfigRegistry>) {
712 if p.is_null() {
713 return;
714 }
715 unsafe {
716 drop(Box::from_raw(p));
717 }
718}
719
720#[unsafe(no_mangle)]
723pub unsafe extern "C" fn net_mesh_public_key_hex(
724 handle: *mut MeshNodeHandle,
725 out_ptr: *mut *mut c_char,
726 out_len: *mut usize,
727) -> c_int {
728 if handle.is_null() || out_ptr.is_null() || out_len.is_null() {
729 return NetError::NullPointer.into();
730 }
731 let h = unsafe { &*handle };
732 let _op = match h.guard.try_enter() {
733 Some(op) => op,
734 None => return NetError::ShuttingDown.into(),
735 };
736 let s = hex::encode(h.inner.public_key());
737 write_string_out(s, out_ptr, out_len)
738}
739
740#[unsafe(no_mangle)]
741pub unsafe extern "C" fn net_mesh_node_id(handle: *mut MeshNodeHandle) -> u64 {
742 if handle.is_null() {
743 return 0;
744 }
745 let h = unsafe { &*handle };
746 let _op = match h.guard.try_enter() {
748 Some(op) => op,
749 None => return 0,
750 };
751 h.inner.node_id()
752}
753
754#[unsafe(no_mangle)]
758pub unsafe extern "C" fn net_mesh_entity_id(handle: *mut MeshNodeHandle, out: *mut u8) -> c_int {
759 if handle.is_null() || out.is_null() {
760 return NetError::NullPointer.into();
761 }
762 let h = unsafe { &*handle };
763 let _op = match h.guard.try_enter() {
764 Some(op) => op,
765 None => return NetError::ShuttingDown.into(),
766 };
767 let bytes = h.inner.entity_id().as_bytes();
768 unsafe {
769 std::ptr::copy_nonoverlapping(bytes.as_ptr(), out, 32);
770 }
771 0
772}
773
774#[unsafe(no_mangle)]
776pub unsafe extern "C" fn net_mesh_connect(
777 handle: *mut MeshNodeHandle,
778 peer_addr: *const c_char,
779 peer_pubkey_hex: *const c_char,
780 peer_node_id: u64,
781) -> c_int {
782 if handle.is_null() || peer_addr.is_null() || peer_pubkey_hex.is_null() {
783 return NetError::NullPointer.into();
784 }
785 let h = unsafe { &*handle };
786 let _op = match h.guard.try_enter() {
787 Some(op) => op,
788 None => return NetError::ShuttingDown.into(),
789 };
790 let Some(addr_s) = (unsafe { c_str_to_string(peer_addr) }) else {
791 return NetError::InvalidUtf8.into();
792 };
793 let addr: std::net::SocketAddr = match addr_s.parse() {
794 Ok(a) => a,
795 Err(_) => return NET_ERR_MESH_HANDSHAKE,
796 };
797 let Some(pk_s) = (unsafe { c_str_to_string(peer_pubkey_hex) }) else {
798 return NetError::InvalidUtf8.into();
799 };
800 let pk_bytes = match hex::decode(pk_s) {
801 Ok(b) => b,
802 Err(_) => return NET_ERR_MESH_HANDSHAKE,
803 };
804 if pk_bytes.len() != 32 {
805 return NET_ERR_MESH_HANDSHAKE;
806 }
807 let mut pk = [0u8; 32];
808 pk.copy_from_slice(&pk_bytes);
809
810 let node = h.inner.clone();
811 match block_on(async move { node.connect(addr, &pk, peer_node_id).await }) {
812 Ok(_) => 0,
813 Err(e) => adapter_err_to_code(&e),
814 }
815}
816
817#[unsafe(no_mangle)]
820pub unsafe extern "C" fn net_mesh_accept(
821 handle: *mut MeshNodeHandle,
822 peer_node_id: u64,
823 out_addr: *mut *mut c_char,
824 out_len: *mut usize,
825) -> c_int {
826 if handle.is_null() || out_addr.is_null() || out_len.is_null() {
827 return NetError::NullPointer.into();
828 }
829 let h = unsafe { &*handle };
830 let _op = match h.guard.try_enter() {
831 Some(op) => op,
832 None => return NetError::ShuttingDown.into(),
833 };
834 let node = h.inner.clone();
835 match block_on(async move { node.accept(peer_node_id).await }) {
836 Ok((addr, _)) => write_string_out(addr.to_string(), out_addr, out_len),
837 Err(e) => adapter_err_to_code(&e),
838 }
839}
840
841#[unsafe(no_mangle)]
842pub unsafe extern "C" fn net_mesh_start(handle: *mut MeshNodeHandle) -> c_int {
843 if handle.is_null() {
844 return NetError::NullPointer.into();
845 }
846 let h = unsafe { &*handle };
847 let _op = match h.guard.try_enter() {
848 Some(op) => op,
849 None => return NetError::ShuttingDown.into(),
850 };
851 let node = h.inner.clone();
852 block_on(async move { node.start() });
855 0
856}
857
858#[unsafe(no_mangle)]
870pub unsafe extern "C" fn net_mesh_shutdown(handle: *mut MeshNodeHandle) -> c_int {
871 if handle.is_null() {
872 return NetError::NullPointer.into();
873 }
874 let h = unsafe { &*handle };
875 let _op = match h.guard.try_enter() {
876 Some(op) => op,
877 None => return NetError::ShuttingDown.into(),
878 };
879 match block_on(async { h.inner.shutdown().await }) {
880 Ok(()) => 0,
881 Err(e) => adapter_err_to_code(&e),
882 }
883}
884
885#[cfg(feature = "nat-traversal")]
907#[unsafe(no_mangle)]
908pub unsafe extern "C" fn net_mesh_nat_type(
909 handle: *mut MeshNodeHandle,
910 out_str: *mut *mut c_char,
911 out_len: *mut usize,
912) -> c_int {
913 if handle.is_null() || out_str.is_null() || out_len.is_null() {
914 return NetError::NullPointer.into();
915 }
916 let h = unsafe { &*handle };
917 let _op = match h.guard.try_enter() {
918 Some(op) => op,
919 None => return NetError::ShuttingDown.into(),
920 };
921 write_string_out(
922 nat_class_to_str(h.inner.nat_class()).to_string(),
923 out_str,
924 out_len,
925 )
926}
927
928#[cfg(feature = "nat-traversal")]
933#[unsafe(no_mangle)]
934pub unsafe extern "C" fn net_mesh_reflex_addr(
935 handle: *mut MeshNodeHandle,
936 out_str: *mut *mut c_char,
937 out_len: *mut usize,
938) -> c_int {
939 if handle.is_null() || out_str.is_null() || out_len.is_null() {
940 return NetError::NullPointer.into();
941 }
942 let h = unsafe { &*handle };
943 let _op = match h.guard.try_enter() {
944 Some(op) => op,
945 None => return NetError::ShuttingDown.into(),
946 };
947 let s = h
948 .inner
949 .reflex_addr()
950 .map(|a| a.to_string())
951 .unwrap_or_default();
952 write_string_out(s, out_str, out_len)
953}
954
955#[cfg(feature = "nat-traversal")]
959#[unsafe(no_mangle)]
960pub unsafe extern "C" fn net_mesh_peer_nat_type(
961 handle: *mut MeshNodeHandle,
962 peer_node_id: u64,
963 out_str: *mut *mut c_char,
964 out_len: *mut usize,
965) -> c_int {
966 if handle.is_null() || out_str.is_null() || out_len.is_null() {
967 return NetError::NullPointer.into();
968 }
969 let h = unsafe { &*handle };
970 let _op = match h.guard.try_enter() {
971 Some(op) => op,
972 None => return NetError::ShuttingDown.into(),
973 };
974 write_string_out(
975 nat_class_to_str(h.inner.peer_nat_class(peer_node_id)).to_string(),
976 out_str,
977 out_len,
978 )
979}
980
981#[cfg(feature = "nat-traversal")]
990#[unsafe(no_mangle)]
991pub unsafe extern "C" fn net_mesh_probe_reflex(
992 handle: *mut MeshNodeHandle,
993 peer_node_id: u64,
994 out_str: *mut *mut c_char,
995 out_len: *mut usize,
996) -> c_int {
997 if handle.is_null() || out_str.is_null() || out_len.is_null() {
998 return NetError::NullPointer.into();
999 }
1000 let h = unsafe { &*handle };
1001 let _op = match h.guard.try_enter() {
1002 Some(op) => op,
1003 None => return NetError::ShuttingDown.into(),
1004 };
1005 let node = h.inner.clone();
1006 match block_on(async move { node.probe_reflex(peer_node_id).await }) {
1007 Ok(addr) => write_string_out(addr.to_string(), out_str, out_len),
1008 Err(e) => traversal_err_to_code(&e),
1009 }
1010}
1011
1012#[cfg(feature = "nat-traversal")]
1017#[unsafe(no_mangle)]
1018pub unsafe extern "C" fn net_mesh_reclassify_nat(handle: *mut MeshNodeHandle) -> c_int {
1019 if handle.is_null() {
1020 return NetError::NullPointer.into();
1021 }
1022 let h = unsafe { &*handle };
1023 let _op = match h.guard.try_enter() {
1024 Some(op) => op,
1025 None => return NetError::ShuttingDown.into(),
1026 };
1027 let node = h.inner.clone();
1028 block_on(async move { node.reclassify_nat().await });
1029 0
1030}
1031
1032#[cfg(feature = "nat-traversal")]
1037#[unsafe(no_mangle)]
1038pub unsafe extern "C" fn net_mesh_traversal_stats(
1039 handle: *mut MeshNodeHandle,
1040 out_punches_attempted: *mut u64,
1041 out_punches_succeeded: *mut u64,
1042 out_relay_fallbacks: *mut u64,
1043) -> c_int {
1044 if handle.is_null() {
1045 return NetError::NullPointer.into();
1046 }
1047 let h = unsafe { &*handle };
1048 let _op = match h.guard.try_enter() {
1049 Some(op) => op,
1050 None => return NetError::ShuttingDown.into(),
1051 };
1052 let snap = h.inner.traversal_stats();
1053 unsafe {
1054 if !out_punches_attempted.is_null() {
1055 *out_punches_attempted = snap.punches_attempted;
1056 }
1057 if !out_punches_succeeded.is_null() {
1058 *out_punches_succeeded = snap.punches_succeeded;
1059 }
1060 if !out_relay_fallbacks.is_null() {
1061 *out_relay_fallbacks = snap.relay_fallbacks;
1062 }
1063 }
1064 0
1065}
1066
1067#[cfg(feature = "nat-traversal")]
1079#[unsafe(no_mangle)]
1080pub unsafe extern "C" fn net_mesh_connect_direct(
1081 handle: *mut MeshNodeHandle,
1082 peer_node_id: u64,
1083 peer_pubkey_hex: *const c_char,
1084 coordinator: u64,
1085) -> c_int {
1086 if handle.is_null() || peer_pubkey_hex.is_null() {
1087 return NetError::NullPointer.into();
1088 }
1089 let h = unsafe { &*handle };
1090 let _op = match h.guard.try_enter() {
1091 Some(op) => op,
1092 None => return NetError::ShuttingDown.into(),
1093 };
1094 let Some(pk_s) = (unsafe { c_str_to_string(peer_pubkey_hex) }) else {
1095 return NetError::InvalidUtf8.into();
1096 };
1097 let pk_bytes = match hex::decode(pk_s) {
1098 Ok(b) => b,
1099 Err(_) => return NET_ERR_MESH_HANDSHAKE,
1100 };
1101 if pk_bytes.len() != 32 {
1102 return NET_ERR_MESH_HANDSHAKE;
1103 }
1104 let mut pk = [0u8; 32];
1105 pk.copy_from_slice(&pk_bytes);
1106
1107 let node = h.inner.clone();
1108 match block_on(async move { node.connect_direct(peer_node_id, &pk, coordinator).await }) {
1109 Ok(_) => 0,
1110 Err(e) => traversal_err_to_code(&e),
1111 }
1112}
1113
1114#[cfg(feature = "nat-traversal")]
1122#[unsafe(no_mangle)]
1123pub unsafe extern "C" fn net_mesh_set_reflex_override(
1124 handle: *mut MeshNodeHandle,
1125 external: *const c_char,
1126) -> c_int {
1127 if handle.is_null() || external.is_null() {
1128 return NetError::NullPointer.into();
1129 }
1130 let h = unsafe { &*handle };
1131 let _op = match h.guard.try_enter() {
1132 Some(op) => op,
1133 None => return NetError::ShuttingDown.into(),
1134 };
1135 let Some(s) = (unsafe { c_str_to_string(external) }) else {
1136 return NetError::InvalidUtf8.into();
1137 };
1138 let Ok(addr) = s.parse::<std::net::SocketAddr>() else {
1139 return NET_ERR_MESH_INIT;
1140 };
1141 h.inner.set_reflex_override(addr);
1142 0
1143}
1144
1145#[cfg(feature = "nat-traversal")]
1153#[unsafe(no_mangle)]
1154pub unsafe extern "C" fn net_mesh_clear_reflex_override(handle: *mut MeshNodeHandle) -> c_int {
1155 if handle.is_null() {
1156 return NetError::NullPointer.into();
1157 }
1158 let h = unsafe { &*handle };
1159 let _op = match h.guard.try_enter() {
1160 Some(op) => op,
1161 None => return NetError::ShuttingDown.into(),
1162 };
1163 h.inner.clear_reflex_override();
1164 0
1165}
1166
1167#[cfg(not(feature = "nat-traversal"))]
1190#[unsafe(no_mangle)]
1191pub unsafe extern "C" fn net_mesh_nat_type(
1192 _handle: *mut MeshNodeHandle,
1193 _out_str: *mut *mut c_char,
1194 _out_len: *mut usize,
1195) -> c_int {
1196 NET_ERR_TRAVERSAL_UNSUPPORTED
1197}
1198
1199#[cfg(not(feature = "nat-traversal"))]
1200#[unsafe(no_mangle)]
1201pub unsafe extern "C" fn net_mesh_reflex_addr(
1202 _handle: *mut MeshNodeHandle,
1203 _out_str: *mut *mut c_char,
1204 _out_len: *mut usize,
1205) -> c_int {
1206 NET_ERR_TRAVERSAL_UNSUPPORTED
1207}
1208
1209#[cfg(not(feature = "nat-traversal"))]
1210#[unsafe(no_mangle)]
1211pub unsafe extern "C" fn net_mesh_peer_nat_type(
1212 _handle: *mut MeshNodeHandle,
1213 _peer_node_id: u64,
1214 _out_str: *mut *mut c_char,
1215 _out_len: *mut usize,
1216) -> c_int {
1217 NET_ERR_TRAVERSAL_UNSUPPORTED
1218}
1219
1220#[cfg(not(feature = "nat-traversal"))]
1221#[unsafe(no_mangle)]
1222pub unsafe extern "C" fn net_mesh_probe_reflex(
1223 _handle: *mut MeshNodeHandle,
1224 _peer_node_id: u64,
1225 _out_str: *mut *mut c_char,
1226 _out_len: *mut usize,
1227) -> c_int {
1228 NET_ERR_TRAVERSAL_UNSUPPORTED
1229}
1230
1231#[cfg(not(feature = "nat-traversal"))]
1232#[unsafe(no_mangle)]
1233pub unsafe extern "C" fn net_mesh_reclassify_nat(_handle: *mut MeshNodeHandle) -> c_int {
1234 NET_ERR_TRAVERSAL_UNSUPPORTED
1235}
1236
1237#[cfg(not(feature = "nat-traversal"))]
1238#[unsafe(no_mangle)]
1239pub unsafe extern "C" fn net_mesh_traversal_stats(
1240 _handle: *mut MeshNodeHandle,
1241 _out_punches_attempted: *mut u64,
1242 _out_punches_succeeded: *mut u64,
1243 _out_relay_fallbacks: *mut u64,
1244) -> c_int {
1245 NET_ERR_TRAVERSAL_UNSUPPORTED
1246}
1247
1248#[cfg(not(feature = "nat-traversal"))]
1249#[unsafe(no_mangle)]
1250pub unsafe extern "C" fn net_mesh_connect_direct(
1251 _handle: *mut MeshNodeHandle,
1252 _peer_node_id: u64,
1253 _peer_pubkey_hex: *const c_char,
1254 _coordinator: u64,
1255) -> c_int {
1256 NET_ERR_TRAVERSAL_UNSUPPORTED
1257}
1258
1259#[cfg(not(feature = "nat-traversal"))]
1260#[unsafe(no_mangle)]
1261pub unsafe extern "C" fn net_mesh_set_reflex_override(
1262 _handle: *mut MeshNodeHandle,
1263 _external: *const c_char,
1264) -> c_int {
1265 NET_ERR_TRAVERSAL_UNSUPPORTED
1266}
1267
1268#[cfg(not(feature = "nat-traversal"))]
1269#[unsafe(no_mangle)]
1270pub unsafe extern "C" fn net_mesh_clear_reflex_override(_handle: *mut MeshNodeHandle) -> c_int {
1271 NET_ERR_TRAVERSAL_UNSUPPORTED
1272}
1273
1274#[derive(Deserialize, Default)]
1279struct StreamOpenConfig {
1280 reliability: Option<String>,
1282 window_bytes: Option<u32>,
1285 fairness_weight: Option<u8>,
1286}
1287
1288pub struct MeshStreamHandle {
1303 stream: ManuallyDrop<CoreStream>,
1304 _node: ManuallyDrop<Arc<MeshNode>>,
1307 guard: HandleGuard,
1308}
1309
1310#[unsafe(no_mangle)]
1311pub unsafe extern "C" fn net_mesh_open_stream(
1312 handle: *mut MeshNodeHandle,
1313 peer_node_id: u64,
1314 stream_id: u64,
1315 config_json: *const c_char,
1316 out_stream: *mut *mut MeshStreamHandle,
1317) -> c_int {
1318 if handle.is_null() || out_stream.is_null() {
1319 return NetError::NullPointer.into();
1320 }
1321 let h = unsafe { &*handle };
1322 let _op = match h.guard.try_enter() {
1323 Some(op) => op,
1324 None => return NetError::ShuttingDown.into(),
1325 };
1326 let cfg_json: StreamOpenConfig = if config_json.is_null() {
1327 StreamOpenConfig::default()
1328 } else {
1329 let Some(s) = (unsafe { c_str_to_string(config_json) }) else {
1330 return NetError::InvalidUtf8.into();
1331 };
1332 match serde_json::from_str(&s) {
1333 Ok(v) => v,
1334 Err(_) => return NetError::InvalidJson.into(),
1335 }
1336 };
1337 let reliability = match cfg_json.reliability.as_deref() {
1338 None | Some("fire_and_forget") => Reliability::FireAndForget,
1339 Some("reliable") => Reliability::Reliable,
1340 Some(_) => return NET_ERR_MESH_TRANSPORT,
1341 };
1342 let window = cfg_json.window_bytes.unwrap_or(DEFAULT_STREAM_WINDOW_BYTES);
1343 let weight = cfg_json.fairness_weight.unwrap_or(1);
1344 let cfg = StreamConfig::new()
1345 .with_reliability(reliability)
1346 .with_window_bytes(window)
1347 .with_fairness_weight(weight);
1348 match h.inner.open_stream(peer_node_id, stream_id, cfg) {
1349 Ok(stream) => {
1350 let node_clone: Arc<MeshNode> = Arc::clone(&h.inner);
1351 let sh = Box::new(MeshStreamHandle {
1352 stream: ManuallyDrop::new(stream),
1353 _node: ManuallyDrop::new(node_clone),
1354 guard: HandleGuard::new(),
1355 });
1356 unsafe {
1357 *out_stream = Box::into_raw(sh);
1358 }
1359 0
1360 }
1361 Err(e) => adapter_err_to_code(&e),
1362 }
1363}
1364
1365#[unsafe(no_mangle)]
1366pub unsafe extern "C" fn net_mesh_stream_free(handle: *mut MeshStreamHandle) {
1367 if handle.is_null() {
1368 return;
1369 }
1370 let h: &MeshStreamHandle = unsafe { &*handle };
1372 if h.guard.begin_free(FFI_HANDLE_FREE_DEADLINE) {
1373 unsafe {
1375 let _stream = ManuallyDrop::take(&mut (*handle).stream);
1379 let node = ManuallyDrop::take(&mut (*handle)._node);
1380 drop(node);
1381 }
1382 } else {
1383 tracing::warn!(
1384 "net_mesh_stream_free: in-flight ops did not drain within deadline; \
1385 leaking inner to avoid use-after-free"
1386 );
1387 }
1388}
1389
1390unsafe fn collect_payloads(
1400 payloads: *const *const u8,
1401 lens: *const usize,
1402 count: usize,
1403) -> Option<Vec<Bytes>> {
1404 let mut out = Vec::with_capacity(count);
1405 for i in 0..count {
1406 let ptr = *payloads.add(i);
1407 let len = *lens.add(i);
1408 if ptr.is_null() {
1409 if len == 0 {
1410 out.push(Bytes::new());
1411 continue;
1412 }
1413 return None;
1414 }
1415 if len > isize::MAX as usize {
1419 return None;
1420 }
1421 let slice = std::slice::from_raw_parts(ptr, len);
1422 out.push(Bytes::copy_from_slice(slice));
1423 }
1424 Some(out)
1425}
1426
1427#[inline]
1435fn handles_match(sh: &MeshStreamHandle, nh: &MeshNodeHandle) -> bool {
1436 Arc::ptr_eq(&sh._node, &nh.inner)
1437}
1438
1439#[unsafe(no_mangle)]
1440pub unsafe extern "C" fn net_mesh_send(
1441 handle: *mut MeshStreamHandle,
1442 payloads: *const *const u8,
1443 lens: *const usize,
1444 count: usize,
1445 node_handle: *mut MeshNodeHandle,
1446) -> c_int {
1447 if handle.is_null() || node_handle.is_null() {
1448 return NetError::NullPointer.into();
1449 }
1450 if count > 0 && (payloads.is_null() || lens.is_null()) {
1451 return NetError::NullPointer.into();
1452 }
1453 let sh = unsafe { &*handle };
1454 let nh = unsafe { &*node_handle };
1455 let _sh_op = match sh.guard.try_enter() {
1458 Some(op) => op,
1459 None => return NetError::ShuttingDown.into(),
1460 };
1461 let _nh_op = match nh.guard.try_enter() {
1462 Some(op) => op,
1463 None => return NetError::ShuttingDown.into(),
1464 };
1465 if !handles_match(sh, nh) {
1466 return NetError::MismatchedHandles.into();
1467 }
1468 let payloads = match unsafe { collect_payloads(payloads, lens, count) } {
1469 Some(v) => v,
1470 None => return NetError::NullPointer.into(),
1471 };
1472 let node = nh.inner.clone();
1473 let stream = sh.stream.clone();
1474 match block_on(async move { node.send_on_stream(&stream, &payloads).await }) {
1475 Ok(()) => 0,
1476 Err(e) => stream_err_to_code(&e),
1477 }
1478}
1479
1480#[unsafe(no_mangle)]
1481pub unsafe extern "C" fn net_mesh_send_with_retry(
1482 handle: *mut MeshStreamHandle,
1483 payloads: *const *const u8,
1484 lens: *const usize,
1485 count: usize,
1486 max_retries: u32,
1487 node_handle: *mut MeshNodeHandle,
1488) -> c_int {
1489 if handle.is_null() || node_handle.is_null() {
1490 return NetError::NullPointer.into();
1491 }
1492 if count > 0 && (payloads.is_null() || lens.is_null()) {
1493 return NetError::NullPointer.into();
1494 }
1495 let sh = unsafe { &*handle };
1496 let nh = unsafe { &*node_handle };
1497 let _sh_op = match sh.guard.try_enter() {
1500 Some(op) => op,
1501 None => return NetError::ShuttingDown.into(),
1502 };
1503 let _nh_op = match nh.guard.try_enter() {
1504 Some(op) => op,
1505 None => return NetError::ShuttingDown.into(),
1506 };
1507 if !handles_match(sh, nh) {
1508 return NetError::MismatchedHandles.into();
1509 }
1510 let payloads = match unsafe { collect_payloads(payloads, lens, count) } {
1511 Some(v) => v,
1512 None => return NetError::NullPointer.into(),
1513 };
1514 let node = nh.inner.clone();
1515 let stream = sh.stream.clone();
1516 match block_on(async move {
1517 node.send_with_retry(&stream, &payloads, max_retries as usize)
1518 .await
1519 }) {
1520 Ok(()) => 0,
1521 Err(e) => stream_err_to_code(&e),
1522 }
1523}
1524
1525#[unsafe(no_mangle)]
1526pub unsafe extern "C" fn net_mesh_send_blocking(
1527 handle: *mut MeshStreamHandle,
1528 payloads: *const *const u8,
1529 lens: *const usize,
1530 count: usize,
1531 node_handle: *mut MeshNodeHandle,
1532) -> c_int {
1533 if handle.is_null() || node_handle.is_null() {
1534 return NetError::NullPointer.into();
1535 }
1536 if count > 0 && (payloads.is_null() || lens.is_null()) {
1537 return NetError::NullPointer.into();
1538 }
1539 let sh = unsafe { &*handle };
1540 let nh = unsafe { &*node_handle };
1541 let _sh_op = match sh.guard.try_enter() {
1544 Some(op) => op,
1545 None => return NetError::ShuttingDown.into(),
1546 };
1547 let _nh_op = match nh.guard.try_enter() {
1548 Some(op) => op,
1549 None => return NetError::ShuttingDown.into(),
1550 };
1551 if !handles_match(sh, nh) {
1552 return NetError::MismatchedHandles.into();
1553 }
1554 let payloads = match unsafe { collect_payloads(payloads, lens, count) } {
1555 Some(v) => v,
1556 None => return NetError::NullPointer.into(),
1557 };
1558 let node = nh.inner.clone();
1559 let stream = sh.stream.clone();
1560 match block_on(async move { node.send_blocking(&stream, &payloads).await }) {
1561 Ok(()) => 0,
1562 Err(e) => stream_err_to_code(&e),
1563 }
1564}
1565
1566#[derive(Serialize)]
1567struct StreamStatsJson {
1568 tx_seq: u64,
1569 rx_seq: u64,
1570 inbound_pending: u64,
1571 last_activity_ns: u64,
1572 active: bool,
1573 backpressure_events: u64,
1574 tx_credit_remaining: u32,
1575 tx_window: u32,
1576 credit_grants_received: u64,
1577 credit_grants_sent: u64,
1578}
1579
1580#[unsafe(no_mangle)]
1581pub unsafe extern "C" fn net_mesh_stream_stats(
1582 node_handle: *mut MeshNodeHandle,
1583 peer_node_id: u64,
1584 stream_id: u64,
1585 out_json: *mut *mut c_char,
1586 out_len: *mut usize,
1587) -> c_int {
1588 if node_handle.is_null() || out_json.is_null() || out_len.is_null() {
1589 return NetError::NullPointer.into();
1590 }
1591 let h = unsafe { &*node_handle };
1592 let _op = match h.guard.try_enter() {
1593 Some(op) => op,
1594 None => return NetError::ShuttingDown.into(),
1595 };
1596 match h.inner.stream_stats(peer_node_id, stream_id) {
1597 Some(s) => {
1598 let js = StreamStatsJson {
1599 tx_seq: s.tx_seq,
1600 rx_seq: s.rx_seq,
1601 inbound_pending: s.inbound_pending,
1602 last_activity_ns: s.last_activity_ns,
1603 active: s.active,
1604 backpressure_events: s.backpressure_events,
1605 tx_credit_remaining: s.tx_credit_remaining,
1606 tx_window: s.tx_window,
1607 credit_grants_received: s.credit_grants_received,
1608 credit_grants_sent: s.credit_grants_sent,
1609 };
1610 write_json_out(&js, out_json, out_len)
1611 }
1612 None => {
1613 write_string_out("null".to_string(), out_json, out_len)
1616 }
1617 }
1618}
1619
1620#[derive(Serialize)]
1625struct RecvEventJson {
1626 id: String,
1627 payload_b64: String,
1629 insertion_ts: u64,
1630 shard_id: u16,
1631}
1632
1633#[unsafe(no_mangle)]
1634pub unsafe extern "C" fn net_mesh_recv_shard(
1635 handle: *mut MeshNodeHandle,
1636 shard_id: u16,
1637 limit: u32,
1638 out_json: *mut *mut c_char,
1639 out_len: *mut usize,
1640) -> c_int {
1641 if handle.is_null() || out_json.is_null() || out_len.is_null() {
1642 return NetError::NullPointer.into();
1643 }
1644 let h = unsafe { &*handle };
1645 let _op = match h.guard.try_enter() {
1646 Some(op) => op,
1647 None => return NetError::ShuttingDown.into(),
1648 };
1649 let node = h.inner.clone();
1650 let result = block_on(async move { node.poll_shard(shard_id, None, limit as usize).await });
1651 let result = match result {
1652 Ok(r) => r,
1653 Err(e) => return adapter_err_to_code(&e),
1654 };
1655 let events: Vec<RecvEventJson> = result
1656 .events
1657 .into_iter()
1658 .map(|e| RecvEventJson {
1659 id: e.id,
1660 payload_b64: encode_b64(&e.raw),
1661 insertion_ts: e.insertion_ts,
1662 shard_id: e.shard_id,
1663 })
1664 .collect();
1665 write_json_out(&events, out_json, out_len)
1666}
1667
1668fn encode_b64(bytes: &[u8]) -> String {
1669 const ALPH: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
1672 let mut s = String::with_capacity(bytes.len().div_ceil(3) * 4);
1673 let mut i = 0;
1674 while i + 3 <= bytes.len() {
1675 let chunk = &bytes[i..i + 3];
1676 s.push(ALPH[(chunk[0] >> 2) as usize] as char);
1677 s.push(ALPH[(((chunk[0] & 0b11) << 4) | (chunk[1] >> 4)) as usize] as char);
1678 s.push(ALPH[(((chunk[1] & 0b1111) << 2) | (chunk[2] >> 6)) as usize] as char);
1679 s.push(ALPH[(chunk[2] & 0b111111) as usize] as char);
1680 i += 3;
1681 }
1682 let rem = bytes.len() - i;
1683 if rem == 1 {
1684 let b = bytes[i];
1685 s.push(ALPH[(b >> 2) as usize] as char);
1686 s.push(ALPH[((b & 0b11) << 4) as usize] as char);
1687 s.push('=');
1688 s.push('=');
1689 } else if rem == 2 {
1690 let b0 = bytes[i];
1691 let b1 = bytes[i + 1];
1692 s.push(ALPH[(b0 >> 2) as usize] as char);
1693 s.push(ALPH[(((b0 & 0b11) << 4) | (b1 >> 4)) as usize] as char);
1694 s.push(ALPH[((b1 & 0b1111) << 2) as usize] as char);
1695 s.push('=');
1696 }
1697 s
1698}
1699
1700#[derive(Deserialize)]
1705struct ChannelConfigInput {
1706 name: String,
1707 visibility: Option<String>,
1708 reliable: Option<bool>,
1709 require_token: Option<bool>,
1710 priority: Option<u8>,
1711 max_rate_pps: Option<u32>,
1712 publish_caps: Option<CapabilityFilterJson>,
1716 subscribe_caps: Option<CapabilityFilterJson>,
1720}
1721
1722fn parse_visibility(s: &str) -> Option<InnerVisibility> {
1723 match s {
1724 "subnet-local" => Some(InnerVisibility::SubnetLocal),
1725 "parent-visible" => Some(InnerVisibility::ParentVisible),
1726 "exported" => Some(InnerVisibility::Exported),
1727 "global" => Some(InnerVisibility::Global),
1728 _ => None,
1729 }
1730}
1731
1732#[unsafe(no_mangle)]
1733pub unsafe extern "C" fn net_mesh_register_channel(
1734 handle: *mut MeshNodeHandle,
1735 config_json: *const c_char,
1736) -> c_int {
1737 if handle.is_null() || config_json.is_null() {
1738 return NetError::NullPointer.into();
1739 }
1740 let h = unsafe { &*handle };
1741 let _op = match h.guard.try_enter() {
1742 Some(op) => op,
1743 None => return NetError::ShuttingDown.into(),
1744 };
1745 let Some(s) = (unsafe { c_str_to_string(config_json) }) else {
1746 return NetError::InvalidUtf8.into();
1747 };
1748 let input: ChannelConfigInput = match serde_json::from_str(&s) {
1749 Ok(v) => v,
1750 Err(_) => return NetError::InvalidJson.into(),
1751 };
1752 let name = match InnerChannelName::new(&input.name) {
1753 Ok(n) => n,
1754 Err(_) => return NET_ERR_CHANNEL,
1755 };
1756 let mut cfg = InnerChannelConfig::new(ChannelId::new(name));
1757 if let Some(v) = input.visibility {
1758 let Some(vis) = parse_visibility(&v) else {
1759 return NET_ERR_CHANNEL;
1760 };
1761 cfg = cfg.with_visibility(vis);
1762 }
1763 if let Some(r) = input.reliable {
1764 cfg = cfg.with_reliable(r);
1765 }
1766 if let Some(t) = input.require_token {
1767 cfg = cfg.with_require_token(t);
1768 }
1769 if let Some(p) = input.priority {
1770 cfg = cfg.with_priority(p);
1771 }
1772 if let Some(pps) = input.max_rate_pps {
1773 cfg = cfg.with_rate_limit(pps);
1774 }
1775 if let Some(filter_json) = input.publish_caps {
1776 cfg = cfg.with_publish_caps(capability_filter_from_json(filter_json));
1777 }
1778 if let Some(filter_json) = input.subscribe_caps {
1779 cfg = cfg.with_subscribe_caps(capability_filter_from_json(filter_json));
1780 }
1781 h.channel_configs.insert(cfg);
1782 0
1783}
1784
1785#[unsafe(no_mangle)]
1786pub unsafe extern "C" fn net_mesh_subscribe_channel(
1787 handle: *mut MeshNodeHandle,
1788 publisher_node_id: u64,
1789 channel: *const c_char,
1790) -> c_int {
1791 subscribe_or_unsubscribe(handle, publisher_node_id, channel, true)
1792}
1793
1794#[unsafe(no_mangle)]
1795pub unsafe extern "C" fn net_mesh_unsubscribe_channel(
1796 handle: *mut MeshNodeHandle,
1797 publisher_node_id: u64,
1798 channel: *const c_char,
1799) -> c_int {
1800 subscribe_or_unsubscribe(handle, publisher_node_id, channel, false)
1801}
1802
1803#[unsafe(no_mangle)]
1810pub unsafe extern "C" fn net_mesh_subscribe_channel_with_token(
1811 handle: *mut MeshNodeHandle,
1812 publisher_node_id: u64,
1813 channel: *const c_char,
1814 token: *const u8,
1815 token_len: usize,
1816) -> c_int {
1817 if handle.is_null() || channel.is_null() || token.is_null() {
1818 return NetError::NullPointer.into();
1819 }
1820 let h = unsafe { &*handle };
1821 let _op = match h.guard.try_enter() {
1822 Some(op) => op,
1823 None => return NetError::ShuttingDown.into(),
1824 };
1825 let Some(s) = (unsafe { c_str_to_string(channel) }) else {
1826 return NetError::InvalidUtf8.into();
1827 };
1828 let name = match InnerChannelName::new(&s) {
1829 Ok(n) => n,
1830 Err(_) => return NET_ERR_CHANNEL,
1831 };
1832 if token_len > isize::MAX as usize {
1834 return NetError::InvalidJson.into();
1835 }
1836 let slice = unsafe { std::slice::from_raw_parts(token, token_len) };
1837 let parsed = match PermissionToken::from_bytes(slice) {
1838 Ok(t) => t,
1839 Err(e) => return token_err_to_code(&e),
1840 };
1841 let node = h.inner.clone();
1842 match block_on(async move {
1843 node.subscribe_channel_with_token(publisher_node_id, name, parsed)
1844 .await
1845 }) {
1846 Ok(()) => 0,
1847 Err(e) => adapter_err_to_channel_code(&e),
1848 }
1849}
1850
1851fn subscribe_or_unsubscribe(
1852 handle: *mut MeshNodeHandle,
1853 publisher_node_id: u64,
1854 channel: *const c_char,
1855 subscribe: bool,
1856) -> c_int {
1857 if handle.is_null() || channel.is_null() {
1858 return NetError::NullPointer.into();
1859 }
1860 let h = unsafe { &*handle };
1861 let _op = match h.guard.try_enter() {
1862 Some(op) => op,
1863 None => return NetError::ShuttingDown.into(),
1864 };
1865 let Some(s) = (unsafe { c_str_to_string(channel) }) else {
1866 return NetError::InvalidUtf8.into();
1867 };
1868 let name = match InnerChannelName::new(&s) {
1869 Ok(n) => n,
1870 Err(_) => return NET_ERR_CHANNEL,
1871 };
1872 let node = h.inner.clone();
1873 let outcome = if subscribe {
1874 block_on(async move { node.subscribe_channel(publisher_node_id, name).await })
1875 } else {
1876 block_on(async move { node.unsubscribe_channel(publisher_node_id, name).await })
1877 };
1878 match outcome {
1879 Ok(()) => 0,
1880 Err(e) => adapter_err_to_channel_code(&e),
1881 }
1882}
1883
1884fn adapter_err_to_channel_code(err: &AdapterError) -> c_int {
1885 if let AdapterError::Connection(msg) = err {
1886 let prefix = "membership request rejected: ";
1887 if let Some(tail) = msg.strip_prefix(prefix) {
1888 if tail.trim() == "Some(Unauthorized)" {
1889 return NET_ERR_CHANNEL_AUTH;
1890 }
1891 }
1892 }
1893 NET_ERR_CHANNEL
1894}
1895
1896#[derive(Deserialize, Default)]
1897struct PublishConfigInput {
1898 reliability: Option<String>,
1899 on_failure: Option<String>,
1900 max_inflight: Option<u32>,
1901}
1902
1903#[derive(Serialize)]
1904struct PublishReportJson {
1905 attempted: u32,
1906 delivered: u32,
1907 errors: Vec<PublishFailureJson>,
1908}
1909
1910#[derive(Serialize)]
1911struct PublishFailureJson {
1912 node_id: u64,
1913 message: String,
1914}
1915
1916fn to_publish_report_json(r: InnerPublishReport) -> PublishReportJson {
1917 PublishReportJson {
1918 attempted: r.attempted as u32,
1919 delivered: r.delivered as u32,
1920 errors: r
1921 .errors
1922 .into_iter()
1923 .map(|(id, e)| PublishFailureJson {
1924 node_id: id,
1925 message: format!("{}", e),
1926 })
1927 .collect(),
1928 }
1929}
1930
1931#[unsafe(no_mangle)]
1932pub unsafe extern "C" fn net_mesh_publish(
1933 handle: *mut MeshNodeHandle,
1934 channel: *const c_char,
1935 payload: *const u8,
1936 len: usize,
1937 config_json: *const c_char,
1938 out_json: *mut *mut c_char,
1939 out_len: *mut usize,
1940) -> c_int {
1941 if handle.is_null() || channel.is_null() || out_json.is_null() || out_len.is_null() {
1942 return NetError::NullPointer.into();
1943 }
1944 let h = unsafe { &*handle };
1945 let _op = match h.guard.try_enter() {
1946 Some(op) => op,
1947 None => return NetError::ShuttingDown.into(),
1948 };
1949 let Some(ch) = (unsafe { c_str_to_string(channel) }) else {
1950 return NetError::InvalidUtf8.into();
1951 };
1952 let name = match InnerChannelName::new(&ch) {
1953 Ok(n) => n,
1954 Err(_) => return NET_ERR_CHANNEL,
1955 };
1956 let cfg_in: PublishConfigInput = if config_json.is_null() {
1957 PublishConfigInput::default()
1958 } else {
1959 let Some(s) = (unsafe { c_str_to_string(config_json) }) else {
1960 return NetError::InvalidUtf8.into();
1961 };
1962 match serde_json::from_str(&s) {
1963 Ok(v) => v,
1964 Err(_) => return NetError::InvalidJson.into(),
1965 }
1966 };
1967 let reliability = match cfg_in.reliability.as_deref() {
1968 None | Some("fire_and_forget") => Reliability::FireAndForget,
1969 Some("reliable") => Reliability::Reliable,
1970 Some(_) => return NET_ERR_CHANNEL,
1971 };
1972 let on_failure = match cfg_in.on_failure.as_deref() {
1973 None | Some("best_effort") => InnerOnFailure::BestEffort,
1974 Some("fail_fast") => InnerOnFailure::FailFast,
1975 Some("collect") => InnerOnFailure::Collect,
1976 Some(_) => return NET_ERR_CHANNEL,
1977 };
1978 let max_inflight = cfg_in.max_inflight.unwrap_or(32) as usize;
1979 let publish_cfg = InnerPublishConfig {
1980 reliability,
1981 on_failure,
1982 max_inflight,
1983 };
1984 let publisher = ChannelPublisher::new(name, publish_cfg);
1985
1986 let bytes = if len == 0 {
1988 Bytes::new()
1989 } else if payload.is_null() {
1990 return NetError::NullPointer.into();
1991 } else if len > isize::MAX as usize {
1992 return NetError::InvalidJson.into();
1994 } else {
1995 Bytes::copy_from_slice(unsafe { std::slice::from_raw_parts(payload, len) })
1996 };
1997
1998 let node = h.inner.clone();
1999 match block_on(async move { node.publish(&publisher, bytes).await }) {
2000 Ok(report) => {
2001 let js = to_publish_report_json(report);
2002 write_json_out(&js, out_json, out_len)
2003 }
2004 Err(e) => adapter_err_to_channel_code(&e),
2005 }
2006}
2007
2008pub struct IdentityHandle {
2022 keypair: ManuallyDrop<Arc<EntityKeypair>>,
2023 cache: ManuallyDrop<Arc<TokenCache>>,
2024 guard: HandleGuard,
2025}
2026
2027fn alloc_bytes(src: &[u8], out_ptr: *mut *mut u8, out_len: *mut usize) -> c_int {
2041 if out_ptr.is_null() || out_len.is_null() {
2042 return NetError::NullPointer.into();
2043 }
2044 let len = src.len();
2045 if len == 0 {
2046 unsafe {
2047 *out_ptr = std::ptr::null_mut();
2048 *out_len = 0;
2049 }
2050 return 0;
2051 }
2052 let layout = match std::alloc::Layout::array::<u8>(len) {
2061 Ok(l) => l,
2062 Err(_) => return NET_ERR_IDENTITY,
2068 };
2069 let ptr = unsafe { std::alloc::alloc(layout) };
2070 if ptr.is_null() {
2071 std::alloc::handle_alloc_error(layout);
2072 }
2073 unsafe {
2074 std::ptr::copy_nonoverlapping(src.as_ptr(), ptr, len);
2075 *out_ptr = ptr;
2076 *out_len = len;
2077 }
2078 0
2079}
2080
2081#[unsafe(no_mangle)]
2096pub unsafe extern "C" fn net_free_bytes(ptr: *mut u8, len: usize) {
2097 if ptr.is_null() || len == 0 {
2098 return;
2099 }
2100 let layout = match std::alloc::Layout::array::<u8>(len) {
2106 Ok(l) => l,
2107 Err(_) => return,
2108 };
2109 unsafe {
2110 std::alloc::dealloc(ptr, layout);
2111 }
2112}
2113
2114fn entity_id_from_bytes(bytes: *const u8, len: usize) -> Option<EntityId> {
2115 if bytes.is_null() || len != 32 {
2116 return None;
2117 }
2118 let slice = unsafe { std::slice::from_raw_parts(bytes, 32) };
2119 let mut arr = [0u8; 32];
2120 arr.copy_from_slice(slice);
2121 Some(EntityId::from_bytes(arr))
2122}
2123
2124fn parse_scope_list(raw: &str) -> Option<TokenScope> {
2125 let values: Vec<String> = serde_json::from_str(raw).ok()?;
2129 let mut acc = TokenScope::NONE;
2130 for s in &values {
2131 acc = acc.union(match s.as_str() {
2132 "publish" => TokenScope::PUBLISH,
2133 "subscribe" => TokenScope::SUBSCRIBE,
2134 "admin" => TokenScope::ADMIN,
2135 "delegate" => TokenScope::DELEGATE,
2136 _ => return None,
2137 });
2138 }
2139 Some(acc)
2140}
2141
2142fn scope_to_strings(scope: TokenScope) -> Vec<&'static str> {
2143 let mut out = Vec::new();
2144 if scope.contains(TokenScope::PUBLISH) {
2145 out.push("publish");
2146 }
2147 if scope.contains(TokenScope::SUBSCRIBE) {
2148 out.push("subscribe");
2149 }
2150 if scope.contains(TokenScope::ADMIN) {
2151 out.push("admin");
2152 }
2153 if scope.contains(TokenScope::DELEGATE) {
2154 out.push("delegate");
2155 }
2156 out
2157}
2158
2159fn channel_name_to_hash(channel: &str) -> Option<ChannelHash> {
2160 InnerChannelName::new(channel).ok().map(|n| n.hash())
2161}
2162
2163#[unsafe(no_mangle)]
2166pub unsafe extern "C" fn net_identity_generate(out_handle: *mut *mut IdentityHandle) -> c_int {
2167 if out_handle.is_null() {
2168 return NetError::NullPointer.into();
2169 }
2170 let handle = Box::new(IdentityHandle {
2171 keypair: ManuallyDrop::new(Arc::new(EntityKeypair::generate())),
2172 cache: ManuallyDrop::new(Arc::new(TokenCache::new())),
2173 guard: HandleGuard::new(),
2174 });
2175 unsafe {
2176 *out_handle = Box::into_raw(handle);
2177 }
2178 0
2179}
2180
2181#[unsafe(no_mangle)]
2185pub unsafe extern "C" fn net_identity_from_seed(
2186 seed: *const u8,
2187 seed_len: usize,
2188 out_handle: *mut *mut IdentityHandle,
2189) -> c_int {
2190 if seed.is_null() || out_handle.is_null() {
2191 return NetError::NullPointer.into();
2192 }
2193 if seed_len != 32 {
2194 return NET_ERR_IDENTITY;
2195 }
2196 let mut arr = [0u8; 32];
2197 arr.copy_from_slice(unsafe { std::slice::from_raw_parts(seed, 32) });
2198 let handle = Box::new(IdentityHandle {
2199 keypair: ManuallyDrop::new(Arc::new(EntityKeypair::from_bytes(arr))),
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)]
2210pub unsafe extern "C" fn net_identity_free(handle: *mut IdentityHandle) {
2211 if handle.is_null() {
2212 return;
2213 }
2214 let h: &IdentityHandle = unsafe { &*handle };
2216 if h.guard.begin_free(FFI_HANDLE_FREE_DEADLINE) {
2217 unsafe {
2219 let mh = &mut *handle;
2220 let kp = ManuallyDrop::take(&mut mh.keypair);
2221 let cache = ManuallyDrop::take(&mut mh.cache);
2222 drop(kp);
2223 drop(cache);
2224 }
2225 } else {
2226 tracing::warn!(
2227 "net_identity_free: in-flight ops did not drain within deadline; \
2228 leaking inner to avoid use-after-free"
2229 );
2230 }
2231}
2232
2233#[unsafe(no_mangle)]
2236pub unsafe extern "C" fn net_identity_to_seed(handle: *mut IdentityHandle, out: *mut u8) -> c_int {
2237 if handle.is_null() || out.is_null() {
2238 return NetError::NullPointer.into();
2239 }
2240 let h = unsafe { &*handle };
2241 let _op = match h.guard.try_enter() {
2242 Some(op) => op,
2243 None => return NetError::ShuttingDown.into(),
2244 };
2245 let seed = h.keypair.secret_bytes();
2246 unsafe {
2247 std::ptr::copy_nonoverlapping(seed.as_ptr(), out, 32);
2248 }
2249 0
2250}
2251
2252#[unsafe(no_mangle)]
2254pub unsafe extern "C" fn net_identity_entity_id(
2255 handle: *mut IdentityHandle,
2256 out: *mut u8,
2257) -> c_int {
2258 if handle.is_null() || out.is_null() {
2259 return NetError::NullPointer.into();
2260 }
2261 let h = unsafe { &*handle };
2262 let _op = match h.guard.try_enter() {
2263 Some(op) => op,
2264 None => return NetError::ShuttingDown.into(),
2265 };
2266 let id = h.keypair.entity_id().as_bytes();
2267 unsafe {
2268 std::ptr::copy_nonoverlapping(id.as_ptr(), out, 32);
2269 }
2270 0
2271}
2272
2273#[unsafe(no_mangle)]
2274pub unsafe extern "C" fn net_identity_node_id(handle: *mut IdentityHandle) -> u64 {
2275 if handle.is_null() {
2276 return 0;
2277 }
2278 let h = unsafe { &*handle };
2279 let _op = match h.guard.try_enter() {
2281 Some(op) => op,
2282 None => return 0,
2283 };
2284 h.keypair.node_id()
2285}
2286
2287#[unsafe(no_mangle)]
2288pub unsafe extern "C" fn net_identity_origin_hash(handle: *mut IdentityHandle) -> u64 {
2289 if handle.is_null() {
2290 return 0;
2291 }
2292 let h = unsafe { &*handle };
2293 let _op = match h.guard.try_enter() {
2295 Some(op) => op,
2296 None => return 0,
2297 };
2298 h.keypair.origin_hash()
2299}
2300
2301#[unsafe(no_mangle)]
2304pub unsafe extern "C" fn net_identity_sign(
2305 handle: *mut IdentityHandle,
2306 msg: *const u8,
2307 len: usize,
2308 out_sig: *mut u8,
2309) -> c_int {
2310 if handle.is_null() || out_sig.is_null() {
2311 return NetError::NullPointer.into();
2312 }
2313 if len > 0 && msg.is_null() {
2314 return NetError::NullPointer.into();
2315 }
2316 let h = unsafe { &*handle };
2317 let _op = match h.guard.try_enter() {
2318 Some(op) => op,
2319 None => return NetError::ShuttingDown.into(),
2320 };
2321 let slice = if len == 0 {
2322 &[][..]
2323 } else if len > isize::MAX as usize {
2324 return NetError::InvalidJson.into();
2326 } else {
2327 unsafe { std::slice::from_raw_parts(msg, len) }
2328 };
2329 let sig = h.keypair.sign(slice).to_bytes();
2330 unsafe {
2331 std::ptr::copy_nonoverlapping(sig.as_ptr(), out_sig, 64);
2332 }
2333 0
2334}
2335
2336#[unsafe(no_mangle)]
2339pub unsafe extern "C" fn net_identity_issue_token(
2340 signer: *mut IdentityHandle,
2341 subject: *const u8,
2342 subject_len: usize,
2343 scope_json: *const c_char,
2344 channel: *const c_char,
2345 ttl_seconds: u32,
2346 delegation_depth: u8,
2347 out_token: *mut *mut u8,
2348 out_token_len: *mut usize,
2349) -> c_int {
2350 if signer.is_null() || out_token.is_null() || out_token_len.is_null() {
2351 return NetError::NullPointer.into();
2352 }
2353 let Some(subject_id) = entity_id_from_bytes(subject, subject_len) else {
2354 return NET_ERR_IDENTITY;
2355 };
2356 let Some(scope_s) = (unsafe { c_str_to_string(scope_json) }) else {
2357 return NetError::InvalidUtf8.into();
2358 };
2359 let Some(scope) = parse_scope_list(&scope_s) else {
2360 return NET_ERR_IDENTITY;
2361 };
2362 let Some(channel_s) = (unsafe { c_str_to_string(channel) }) else {
2363 return NetError::InvalidUtf8.into();
2364 };
2365 let Some(channel_hash) = channel_name_to_hash(&channel_s) else {
2366 return NET_ERR_IDENTITY;
2367 };
2368 let h = unsafe { &*signer };
2369 let _op = match h.guard.try_enter() {
2373 Some(op) => op,
2374 None => return NetError::ShuttingDown.into(),
2375 };
2376 let token = match PermissionToken::try_issue(
2382 &h.keypair,
2383 subject_id,
2384 scope,
2385 channel_hash,
2386 u64::from(ttl_seconds),
2387 delegation_depth,
2388 ) {
2389 Ok(t) => t,
2390 Err(e) => return token_err_to_code(&e),
2391 };
2392 alloc_bytes(&token.to_bytes(), out_token, out_token_len)
2393}
2394
2395#[unsafe(no_mangle)]
2399pub unsafe extern "C" fn net_identity_install_token(
2400 handle: *mut IdentityHandle,
2401 token: *const u8,
2402 len: usize,
2403) -> c_int {
2404 if handle.is_null() || token.is_null() {
2405 return NetError::NullPointer.into();
2406 }
2407 if len > isize::MAX as usize {
2409 return NetError::InvalidJson.into();
2410 }
2411 let slice = unsafe { std::slice::from_raw_parts(token, len) };
2412 let parsed = match PermissionToken::from_bytes(slice) {
2413 Ok(t) => t,
2414 Err(e) => return token_err_to_code(&e),
2415 };
2416 let h = unsafe { &*handle };
2417 let _op = match h.guard.try_enter() {
2418 Some(op) => op,
2419 None => return NetError::ShuttingDown.into(),
2420 };
2421 match h.cache.insert(parsed) {
2422 Ok(()) => 0,
2423 Err(e) => token_err_to_code(&e),
2424 }
2425}
2426
2427#[unsafe(no_mangle)]
2431pub unsafe extern "C" fn net_identity_lookup_token(
2432 handle: *mut IdentityHandle,
2433 subject: *const u8,
2434 subject_len: usize,
2435 channel: *const c_char,
2436 out_token: *mut *mut u8,
2437 out_token_len: *mut usize,
2438) -> c_int {
2439 if handle.is_null() || out_token.is_null() || out_token_len.is_null() {
2440 return NetError::NullPointer.into();
2441 }
2442 let Some(subject_id) = entity_id_from_bytes(subject, subject_len) else {
2443 return NET_ERR_IDENTITY;
2444 };
2445 let Some(channel_s) = (unsafe { c_str_to_string(channel) }) else {
2446 return NetError::InvalidUtf8.into();
2447 };
2448 let Some(channel_hash) = channel_name_to_hash(&channel_s) else {
2449 return NET_ERR_IDENTITY;
2450 };
2451 let h = unsafe { &*handle };
2452 let _op = match h.guard.try_enter() {
2453 Some(op) => op,
2454 None => return NetError::ShuttingDown.into(),
2455 };
2456 match h.cache.get(&subject_id, channel_hash) {
2457 Some(token) => alloc_bytes(&token.to_bytes(), out_token, out_token_len),
2458 None => {
2459 unsafe {
2460 *out_token = std::ptr::null_mut();
2461 *out_token_len = 0;
2462 }
2463 0
2464 }
2465 }
2466}
2467
2468#[unsafe(no_mangle)]
2469pub unsafe extern "C" fn net_identity_token_cache_len(handle: *mut IdentityHandle) -> u32 {
2470 if handle.is_null() {
2471 return 0;
2472 }
2473 let h = unsafe { &*handle };
2474 let _op = match h.guard.try_enter() {
2476 Some(op) => op,
2477 None => return 0,
2478 };
2479 h.cache.len() as u32
2480}
2481
2482#[derive(Serialize)]
2487struct ParsedTokenJson {
2488 issuer_hex: String,
2489 subject_hex: String,
2490 scope: Vec<&'static str>,
2491 channel_hash: ChannelHash,
2492 not_before: u64,
2493 not_after: u64,
2494 delegation_depth: u8,
2495 nonce: u64,
2496 signature_hex: String,
2497}
2498
2499#[unsafe(no_mangle)]
2504pub unsafe extern "C" fn net_parse_token(
2505 token: *const u8,
2506 len: usize,
2507 out_json: *mut *mut c_char,
2508 out_len: *mut usize,
2509) -> c_int {
2510 if token.is_null() || out_json.is_null() || out_len.is_null() {
2511 return NetError::NullPointer.into();
2512 }
2513 if len > isize::MAX as usize {
2515 return NetError::InvalidJson.into();
2516 }
2517 let slice = unsafe { std::slice::from_raw_parts(token, len) };
2518 let parsed = match PermissionToken::from_bytes(slice) {
2519 Ok(t) => t,
2520 Err(e) => return token_err_to_code(&e),
2521 };
2522 let out = ParsedTokenJson {
2523 issuer_hex: hex::encode(parsed.issuer.as_bytes()),
2524 subject_hex: hex::encode(parsed.subject.as_bytes()),
2525 scope: scope_to_strings(parsed.scope),
2526 channel_hash: parsed.channel_hash,
2527 not_before: parsed.not_before,
2528 not_after: parsed.not_after,
2529 delegation_depth: parsed.delegation_depth,
2530 nonce: parsed.nonce,
2531 signature_hex: hex::encode(parsed.signature),
2532 };
2533 write_json_out(&out, out_json, out_len)
2534}
2535
2536#[unsafe(no_mangle)]
2540pub unsafe extern "C" fn net_verify_token(
2541 token: *const u8,
2542 len: usize,
2543 out_ok: *mut c_int,
2544) -> c_int {
2545 if token.is_null() || out_ok.is_null() {
2546 return NetError::NullPointer.into();
2547 }
2548 if len > isize::MAX as usize {
2550 return NetError::InvalidJson.into();
2551 }
2552 let slice = unsafe { std::slice::from_raw_parts(token, len) };
2553 let parsed = match PermissionToken::from_bytes(slice) {
2554 Ok(t) => t,
2555 Err(e) => return token_err_to_code(&e),
2556 };
2557 unsafe {
2558 *out_ok = if parsed.verify().is_ok() { 1 } else { 0 };
2559 }
2560 0
2561}
2562
2563#[unsafe(no_mangle)]
2568pub unsafe extern "C" fn net_token_is_expired(
2569 token: *const u8,
2570 len: usize,
2571 out_expired: *mut c_int,
2572) -> c_int {
2573 if token.is_null() || out_expired.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_expired = if parsed.is_expired() { 1 } else { 0 };
2587 }
2588 0
2589}
2590
2591#[unsafe(no_mangle)]
2594pub unsafe extern "C" fn net_delegate_token(
2595 signer: *mut IdentityHandle,
2596 parent: *const u8,
2597 parent_len: usize,
2598 new_subject: *const u8,
2599 new_subject_len: usize,
2600 restricted_scope_json: *const c_char,
2601 out_token: *mut *mut u8,
2602 out_token_len: *mut usize,
2603) -> c_int {
2604 if signer.is_null()
2605 || parent.is_null()
2606 || new_subject.is_null()
2607 || restricted_scope_json.is_null()
2608 || out_token.is_null()
2609 || out_token_len.is_null()
2610 {
2611 return NetError::NullPointer.into();
2612 }
2613 if parent_len > isize::MAX as usize {
2615 return NetError::InvalidJson.into();
2616 }
2617 let parent_slice = unsafe { std::slice::from_raw_parts(parent, parent_len) };
2618 let parent_tok = match PermissionToken::from_bytes(parent_slice) {
2619 Ok(t) => t,
2620 Err(e) => return token_err_to_code(&e),
2621 };
2622 let Some(subject_id) = entity_id_from_bytes(new_subject, new_subject_len) else {
2623 return NET_ERR_IDENTITY;
2624 };
2625 let Some(scope_s) = (unsafe { c_str_to_string(restricted_scope_json) }) else {
2626 return NetError::InvalidUtf8.into();
2627 };
2628 let Some(scope) = parse_scope_list(&scope_s) else {
2629 return NET_ERR_IDENTITY;
2630 };
2631 let h = unsafe { &*signer };
2632 let _op = match h.guard.try_enter() {
2636 Some(op) => op,
2637 None => return NetError::ShuttingDown.into(),
2638 };
2639 match parent_tok.delegate(&h.keypair, subject_id, scope) {
2640 Ok(child) => alloc_bytes(&child.to_bytes(), out_token, out_token_len),
2641 Err(e) => token_err_to_code(&e),
2642 }
2643}
2644
2645#[unsafe(no_mangle)]
2650pub unsafe extern "C" fn net_channel_hash(channel: *const c_char, out_hash: *mut u64) -> c_int {
2651 if channel.is_null() || out_hash.is_null() {
2652 return NetError::NullPointer.into();
2653 }
2654 let Some(s) = (unsafe { c_str_to_string(channel) }) else {
2655 return NetError::InvalidUtf8.into();
2656 };
2657 let Some(hash) = channel_name_to_hash(&s) else {
2658 return NET_ERR_IDENTITY;
2659 };
2660 unsafe {
2661 *out_hash = hash;
2662 }
2663 0
2664}
2665
2666use crate::adapter::net::behavior::capability::{
2673 AcceleratorInfo, AcceleratorType, CapabilityFilter, CapabilitySet, GpuInfo, GpuVendor,
2674 HardwareCapabilities, Modality, ModelCapability, ResourceLimits, SoftwareCapabilities,
2675 ToolCapability, TAG_SCOPE_REGION_PREFIX, TAG_SCOPE_SUBNET_LOCAL, TAG_SCOPE_TENANT_PREFIX,
2676};
2677
2678fn parse_gpu_vendor_cap(s: &str) -> GpuVendor {
2681 match s.to_ascii_lowercase().as_str() {
2682 "nvidia" => GpuVendor::Nvidia,
2683 "amd" => GpuVendor::Amd,
2684 "intel" => GpuVendor::Intel,
2685 "apple" => GpuVendor::Apple,
2686 "qualcomm" => GpuVendor::Qualcomm,
2687 _ => GpuVendor::Unknown,
2688 }
2689}
2690
2691fn gpu_vendor_to_string_cap(v: GpuVendor) -> &'static str {
2692 match v {
2693 GpuVendor::Nvidia => "nvidia",
2694 GpuVendor::Amd => "amd",
2695 GpuVendor::Intel => "intel",
2696 GpuVendor::Apple => "apple",
2697 GpuVendor::Qualcomm => "qualcomm",
2698 GpuVendor::Unknown => "unknown",
2699 }
2700}
2701
2702fn parse_modality_cap(s: &str) -> Option<Modality> {
2703 match s.to_ascii_lowercase().as_str() {
2704 "text" => Some(Modality::Text),
2705 "image" => Some(Modality::Image),
2706 "audio" => Some(Modality::Audio),
2707 "video" => Some(Modality::Video),
2708 "code" => Some(Modality::Code),
2709 "embedding" => Some(Modality::Embedding),
2710 "tool-use" | "tool_use" | "tooluse" => Some(Modality::ToolUse),
2711 _ => None,
2720 }
2721}
2722
2723fn parse_accelerator_type_cap(s: &str) -> AcceleratorType {
2724 match s.to_ascii_lowercase().as_str() {
2725 "tpu" => AcceleratorType::Tpu,
2726 "npu" => AcceleratorType::Npu,
2727 "fpga" => AcceleratorType::Fpga,
2728 "asic" => AcceleratorType::Asic,
2729 "dsp" => AcceleratorType::Dsp,
2730 _ => AcceleratorType::Unknown,
2731 }
2732}
2733
2734#[derive(Deserialize, Default)]
2737struct CapabilitySetJson {
2738 #[serde(default)]
2739 hardware: Option<HardwareJson>,
2740 #[serde(default)]
2741 software: Option<SoftwareJson>,
2742 #[serde(default)]
2743 models: Vec<ModelJson>,
2744 #[serde(default)]
2745 tools: Vec<ToolJson>,
2746 #[serde(default)]
2747 tags: Vec<String>,
2748 #[serde(default)]
2749 limits: Option<LimitsJson>,
2750}
2751
2752#[derive(Deserialize, Default)]
2753struct HardwareJson {
2754 cpu_cores: Option<u32>,
2755 cpu_threads: Option<u32>,
2756 memory_gb: Option<u32>,
2757 gpu: Option<GpuJson>,
2758 #[serde(default)]
2759 additional_gpus: Vec<GpuJson>,
2760 storage_gb: Option<u64>,
2761 network_gbps: Option<u32>,
2762 #[serde(default)]
2763 accelerators: Vec<AcceleratorJson>,
2764}
2765
2766#[derive(Deserialize)]
2767struct GpuJson {
2768 vendor: Option<String>,
2769 #[serde(default)]
2770 model: String,
2771 #[serde(default)]
2772 vram_gb: u32,
2773 compute_units: Option<u32>,
2774 tensor_cores: Option<u32>,
2775 fp16_tflops_x10: Option<u32>,
2776}
2777
2778#[derive(Deserialize)]
2779struct AcceleratorJson {
2780 #[serde(default)]
2781 kind: String,
2782 #[serde(default)]
2783 model: String,
2784 memory_gb: Option<u32>,
2785 tops_x10: Option<u32>,
2786}
2787
2788#[derive(Deserialize, Default)]
2789struct SoftwareJson {
2790 os: Option<String>,
2791 os_version: Option<String>,
2792 #[serde(default)]
2793 runtimes: Vec<Vec<String>>,
2794 #[serde(default)]
2795 frameworks: Vec<Vec<String>>,
2796 cuda_version: Option<String>,
2797 #[serde(default)]
2798 drivers: Vec<Vec<String>>,
2799}
2800
2801#[derive(Deserialize)]
2802struct ModelJson {
2803 #[serde(default)]
2804 model_id: String,
2805 #[serde(default)]
2806 family: String,
2807 parameters_b_x10: Option<u32>,
2808 context_length: Option<u32>,
2809 quantization: Option<String>,
2810 #[serde(default)]
2811 modalities: Vec<String>,
2812 tokens_per_sec: Option<u32>,
2813 loaded: Option<bool>,
2814}
2815
2816#[derive(Deserialize)]
2817struct ToolJson {
2818 #[serde(default)]
2819 tool_id: String,
2820 #[serde(default)]
2821 name: String,
2822 version: Option<String>,
2823 input_schema: Option<String>,
2824 output_schema: Option<String>,
2825 #[serde(default)]
2826 requires: Vec<String>,
2827 estimated_time_ms: Option<u32>,
2828 stateless: Option<bool>,
2829}
2830
2831#[derive(Deserialize, Default)]
2832struct LimitsJson {
2833 max_concurrent_requests: Option<u32>,
2834 max_tokens_per_request: Option<u32>,
2835 rate_limit_rpm: Option<u32>,
2836 max_batch_size: Option<u32>,
2837 max_input_bytes: Option<u32>,
2838 max_output_bytes: Option<u32>,
2839}
2840
2841#[derive(Deserialize, Default)]
2842struct CapabilityFilterJson {
2843 #[serde(default)]
2844 require_tags: Vec<String>,
2845 #[serde(default)]
2846 require_models: Vec<String>,
2847 #[serde(default)]
2848 require_tools: Vec<String>,
2849 min_memory_gb: Option<u32>,
2850 require_gpu: Option<bool>,
2851 gpu_vendor: Option<String>,
2852 min_vram_gb: Option<u32>,
2853 min_context_length: Option<u32>,
2854 #[serde(default)]
2855 require_modalities: Vec<String>,
2856}
2857
2858fn pair_vec(xs: Vec<Vec<String>>) -> Vec<(String, String)> {
2861 xs.into_iter()
2862 .filter_map(|mut p| {
2863 if p.len() >= 2 {
2864 Some((std::mem::take(&mut p[0]), std::mem::take(&mut p[1])))
2865 } else {
2866 None
2867 }
2868 })
2869 .collect()
2870}
2871
2872#[inline]
2878fn saturating_u16_cap(v: u32) -> u16 {
2879 v.min(u16::MAX as u32) as u16
2880}
2881
2882fn gpu_info_from_json(g: GpuJson) -> GpuInfo {
2883 let vendor = g
2884 .vendor
2885 .as_deref()
2886 .map(parse_gpu_vendor_cap)
2887 .unwrap_or(GpuVendor::Unknown);
2888 let mut info = GpuInfo::new(vendor, g.model, g.vram_gb);
2889 if let Some(cu) = g.compute_units {
2890 info = info.with_compute_units(saturating_u16_cap(cu));
2891 }
2892 if let Some(tc) = g.tensor_cores {
2893 info = info.with_tensor_cores(saturating_u16_cap(tc));
2894 }
2895 if let Some(tf) = g.fp16_tflops_x10 {
2896 let tf_capped = saturating_u16_cap(tf);
2910 info = info.with_fp16_tflops(tf_capped as f32 / 10.0);
2911 }
2912 info
2913}
2914
2915fn accelerator_from_json(a: AcceleratorJson) -> AcceleratorInfo {
2916 AcceleratorInfo {
2917 accel_type: parse_accelerator_type_cap(&a.kind),
2918 model: a.model,
2919 memory_gb: a.memory_gb.unwrap_or(0),
2920 tops_x10: a.tops_x10.map(saturating_u16_cap).unwrap_or(0),
2921 }
2922}
2923
2924fn hardware_from_json(h: HardwareJson) -> HardwareCapabilities {
2925 let mut hw = HardwareCapabilities::new();
2926 match (h.cpu_cores, h.cpu_threads) {
2927 (Some(c), Some(t)) => hw = hw.with_cpu(saturating_u16_cap(c), saturating_u16_cap(t)),
2928 (Some(c), None) => {
2929 let c16 = saturating_u16_cap(c);
2930 hw = hw.with_cpu(c16, c16);
2931 }
2932 _ => {}
2933 }
2934 if let Some(mb) = h.memory_gb {
2935 hw = hw.with_memory(mb);
2936 }
2937 if let Some(g) = h.gpu {
2938 hw = hw.with_gpu(gpu_info_from_json(g));
2939 }
2940 for g in h.additional_gpus {
2941 hw = hw.add_gpu(gpu_info_from_json(g));
2942 }
2943 if let Some(mb) = h.storage_gb {
2944 hw = hw.with_storage(mb);
2945 }
2946 if let Some(gbps) = h.network_gbps {
2947 hw = hw.with_network(gbps);
2948 }
2949 for a in h.accelerators {
2950 hw = hw.add_accelerator(accelerator_from_json(a));
2951 }
2952 hw
2953}
2954
2955fn software_from_json(s: SoftwareJson) -> SoftwareCapabilities {
2956 let mut sw = SoftwareCapabilities::new()
2957 .with_os(s.os.unwrap_or_default(), s.os_version.unwrap_or_default());
2958 for (k, v) in pair_vec(s.runtimes) {
2959 sw = sw.add_runtime(k, v);
2960 }
2961 for (k, v) in pair_vec(s.frameworks) {
2962 sw = sw.add_framework(k, v);
2963 }
2964 if let Some(c) = s.cuda_version {
2965 sw = sw.with_cuda(c);
2966 }
2967 sw.drivers = pair_vec(s.drivers);
2968 sw
2969}
2970
2971fn model_from_json(m: ModelJson) -> ModelCapability {
2972 let mut mc = ModelCapability::new(m.model_id, m.family);
2973 if let Some(p) = m.parameters_b_x10 {
2974 mc.parameters_b_x10 = p;
2975 }
2976 if let Some(c) = m.context_length {
2977 mc = mc.with_context_length(c);
2978 }
2979 if let Some(q) = m.quantization {
2980 mc = mc.with_quantization(q);
2981 }
2982 for modality in m.modalities {
2983 match parse_modality_cap(&modality) {
2984 Some(parsed) => mc = mc.add_modality(parsed),
2985 None => {
2986 tracing::warn!(
2987 modality = %modality,
2988 "announce_capabilities: unknown modality string (typo?), \
2989 skipping rather than the pre-fix silent fallback to Text — \
2990 advertising a Text capability the node doesn't actually \
2991 have produced wrong scheduling decisions on the receiver",
2992 );
2993 }
2994 }
2995 }
2996 if let Some(t) = m.tokens_per_sec {
2997 mc = mc.with_tokens_per_sec(t);
2998 }
2999 if let Some(l) = m.loaded {
3000 mc = mc.with_loaded(l);
3001 }
3002 mc
3003}
3004
3005fn tool_from_json(t: ToolJson) -> ToolCapability {
3006 let mut tc = ToolCapability::new(t.tool_id, t.name);
3007 if let Some(v) = t.version {
3008 tc = tc.with_version(v);
3009 }
3010 if let Some(s) = t.input_schema {
3011 tc = tc.with_input_schema(s);
3012 }
3013 if let Some(s) = t.output_schema {
3014 tc = tc.with_output_schema(s);
3015 }
3016 for r in t.requires {
3017 tc = tc.requires(r);
3018 }
3019 if let Some(ms) = t.estimated_time_ms {
3020 tc = tc.with_estimated_time(ms);
3021 }
3022 if let Some(st) = t.stateless {
3023 tc = tc.with_stateless(st);
3024 }
3025 tc
3026}
3027
3028fn limits_from_json(l: LimitsJson) -> ResourceLimits {
3029 let mut rl = ResourceLimits::new();
3030 if let Some(n) = l.max_concurrent_requests {
3031 rl = rl.with_max_concurrent(n);
3032 }
3033 if let Some(n) = l.max_tokens_per_request {
3034 rl = rl.with_max_tokens(n);
3035 }
3036 if let Some(n) = l.rate_limit_rpm {
3037 rl = rl.with_rate_limit(n);
3038 }
3039 if let Some(n) = l.max_batch_size {
3040 rl = rl.with_max_batch(n);
3041 }
3042 if let Some(n) = l.max_input_bytes {
3043 rl.max_input_bytes = n;
3044 }
3045 if let Some(n) = l.max_output_bytes {
3046 rl.max_output_bytes = n;
3047 }
3048 rl
3049}
3050
3051fn capability_set_from_json(caps: CapabilitySetJson) -> CapabilitySet {
3052 let mut cs = CapabilitySet::new();
3053 if let Some(h) = caps.hardware {
3054 cs = cs.with_hardware(hardware_from_json(h));
3055 }
3056 if let Some(s) = caps.software {
3057 cs = cs.with_software(software_from_json(s));
3058 }
3059 for m in caps.models {
3060 cs = cs.add_model(model_from_json(m));
3061 }
3062 for t in caps.tools {
3063 cs = cs.add_tool(tool_from_json(t));
3064 }
3065 for tag in caps.tags {
3073 if tag == TAG_SCOPE_SUBNET_LOCAL {
3074 cs = cs.with_subnet_local_scope();
3075 } else if let Some(id) = tag.strip_prefix(TAG_SCOPE_TENANT_PREFIX) {
3076 cs = cs.with_tenant_scope(id);
3077 } else if let Some(name) = tag.strip_prefix(TAG_SCOPE_REGION_PREFIX) {
3078 cs = cs.with_region_scope(name);
3079 } else {
3080 cs = cs.add_tag(tag);
3081 }
3082 }
3083 if let Some(l) = caps.limits {
3084 cs = cs.with_limits(limits_from_json(l));
3085 }
3086 cs
3087}
3088
3089fn capability_filter_from_json(f: CapabilityFilterJson) -> CapabilityFilter {
3090 let mut cf = CapabilityFilter::new();
3091 for t in f.require_tags {
3092 cf = cf.require_tag(t);
3093 }
3094 for m in f.require_models {
3095 cf = cf.require_model(m);
3096 }
3097 for t in f.require_tools {
3098 cf = cf.require_tool(t);
3099 }
3100 if let Some(mb) = f.min_memory_gb {
3101 cf = cf.with_min_memory(mb);
3102 }
3103 if f.require_gpu.unwrap_or(false) {
3104 cf = cf.require_gpu();
3105 }
3106 if let Some(v) = f.gpu_vendor {
3107 cf = cf.with_gpu_vendor(parse_gpu_vendor_cap(&v));
3108 }
3109 if let Some(mb) = f.min_vram_gb {
3110 cf = cf.with_min_vram(mb);
3111 }
3112 if let Some(n) = f.min_context_length {
3113 cf = cf.with_min_context(n);
3114 }
3115 for m in f.require_modalities {
3116 match parse_modality_cap(&m) {
3117 Some(parsed) => cf = cf.require_modality(parsed),
3118 None => {
3119 tracing::warn!(
3132 modality = %m,
3133 "find_nodes: unknown modality string in require_modalities \
3134 filter (typo?), dropping the constraint; the resulting \
3135 filter is too permissive — pre-fix it was silently \
3136 re-interpreted as `require Text`, which returned the \
3137 wrong nodes",
3138 );
3139 }
3140 }
3141 }
3142 cf
3143}
3144
3145pub(crate) const NET_ERR_CAPABILITY: c_int = -128;
3148
3149#[unsafe(no_mangle)]
3156pub unsafe extern "C" fn net_mesh_announce_capabilities(
3157 handle: *mut MeshNodeHandle,
3158 caps_json: *const c_char,
3159) -> c_int {
3160 if handle.is_null() || caps_json.is_null() {
3161 return NetError::NullPointer.into();
3162 }
3163 let h = unsafe { &*handle };
3164 let _op = match h.guard.try_enter() {
3165 Some(op) => op,
3166 None => return NetError::ShuttingDown.into(),
3167 };
3168 let Some(s) = (unsafe { c_str_to_string(caps_json) }) else {
3169 return NetError::InvalidUtf8.into();
3170 };
3171 let parsed: CapabilitySetJson = match serde_json::from_str(&s) {
3172 Ok(v) => v,
3173 Err(_) => return NetError::InvalidJson.into(),
3174 };
3175 let caps = capability_set_from_json(parsed);
3176 let node = h.inner.clone();
3177 match block_on(async move { node.announce_capabilities(caps).await }) {
3178 Ok(()) => 0,
3179 Err(_) => NET_ERR_CAPABILITY,
3180 }
3181}
3182
3183#[unsafe(no_mangle)]
3186pub unsafe extern "C" fn net_mesh_find_nodes(
3187 handle: *mut MeshNodeHandle,
3188 filter_json: *const c_char,
3189 out_json: *mut *mut c_char,
3190 out_len: *mut usize,
3191) -> c_int {
3192 if handle.is_null() || filter_json.is_null() || out_json.is_null() || out_len.is_null() {
3193 return NetError::NullPointer.into();
3194 }
3195 let h = unsafe { &*handle };
3196 let _op = match h.guard.try_enter() {
3197 Some(op) => op,
3198 None => return NetError::ShuttingDown.into(),
3199 };
3200 let Some(s) = (unsafe { c_str_to_string(filter_json) }) else {
3201 return NetError::InvalidUtf8.into();
3202 };
3203 let parsed: CapabilityFilterJson = match serde_json::from_str(&s) {
3204 Ok(v) => v,
3205 Err(_) => return NetError::InvalidJson.into(),
3206 };
3207 let filter = capability_filter_from_json(parsed);
3208 let ids = h.inner.find_nodes_by_filter(&filter);
3209 write_json_out(&ids, out_json, out_len)
3210}
3211
3212#[derive(serde::Deserialize)]
3229struct ScopeFilterJson {
3230 kind: String,
3231 #[serde(default)]
3232 tenant: Option<String>,
3233 #[serde(default)]
3234 tenants: Option<Vec<String>>,
3235 #[serde(default)]
3236 region: Option<String>,
3237 #[serde(default)]
3238 regions: Option<Vec<String>>,
3239}
3240
3241enum ScopeFilterOwned {
3247 Any,
3248 GlobalOnly,
3249 SameSubnet,
3250 Tenant(String),
3251 Tenants(Vec<String>),
3252 Region(String),
3253 Regions(Vec<String>),
3254}
3255
3256fn scope_filter_from_json(f: ScopeFilterJson) -> ScopeFilterOwned {
3257 match f.kind.as_str() {
3258 "any" => ScopeFilterOwned::Any,
3259 "global_only" | "globalOnly" => ScopeFilterOwned::GlobalOnly,
3260 "same_subnet" | "sameSubnet" => ScopeFilterOwned::SameSubnet,
3261 "tenant" => match f.tenant {
3262 Some(t) if !t.is_empty() => ScopeFilterOwned::Tenant(t),
3263 _ => ScopeFilterOwned::Any,
3264 },
3265 "tenants" => match f.tenants {
3266 Some(ts) => {
3272 let cleaned: Vec<String> = ts.into_iter().filter(|t| !t.is_empty()).collect();
3273 if cleaned.is_empty() {
3274 ScopeFilterOwned::Any
3275 } else {
3276 ScopeFilterOwned::Tenants(cleaned)
3277 }
3278 }
3279 None => ScopeFilterOwned::Any,
3280 },
3281 "region" => match f.region {
3282 Some(r) if !r.is_empty() => ScopeFilterOwned::Region(r),
3283 _ => ScopeFilterOwned::Any,
3284 },
3285 "regions" => match f.regions {
3286 Some(rs) => {
3288 let cleaned: Vec<String> = rs.into_iter().filter(|r| !r.is_empty()).collect();
3289 if cleaned.is_empty() {
3290 ScopeFilterOwned::Any
3291 } else {
3292 ScopeFilterOwned::Regions(cleaned)
3293 }
3294 }
3295 None => ScopeFilterOwned::Any,
3296 },
3297 _ => ScopeFilterOwned::Any,
3298 }
3299}
3300
3301fn with_scope_filter<R>(
3306 owned: &ScopeFilterOwned,
3307 f: impl FnOnce(&crate::adapter::net::behavior::capability::ScopeFilter<'_>) -> R,
3308) -> R {
3309 use crate::adapter::net::behavior::capability::ScopeFilter as F;
3310 match owned {
3311 ScopeFilterOwned::Any => f(&F::Any),
3312 ScopeFilterOwned::GlobalOnly => f(&F::GlobalOnly),
3313 ScopeFilterOwned::SameSubnet => f(&F::SameSubnet),
3314 ScopeFilterOwned::Tenant(t) => f(&F::Tenant(t.as_str())),
3315 ScopeFilterOwned::Tenants(ts) => {
3316 let refs: Vec<&str> = ts.iter().map(|s| s.as_str()).collect();
3317 f(&F::Tenants(refs.as_slice()))
3318 }
3319 ScopeFilterOwned::Region(r) => f(&F::Region(r.as_str())),
3320 ScopeFilterOwned::Regions(rs) => {
3321 let refs: Vec<&str> = rs.iter().map(|s| s.as_str()).collect();
3322 f(&F::Regions(refs.as_slice()))
3323 }
3324 }
3325}
3326
3327#[unsafe(no_mangle)]
3350pub unsafe extern "C" fn net_mesh_find_nodes_scoped(
3351 handle: *mut MeshNodeHandle,
3352 filter_json: *const c_char,
3353 scope_json: *const c_char,
3354 out_json: *mut *mut c_char,
3355 out_len: *mut usize,
3356) -> c_int {
3357 if handle.is_null()
3358 || filter_json.is_null()
3359 || scope_json.is_null()
3360 || out_json.is_null()
3361 || out_len.is_null()
3362 {
3363 return NetError::NullPointer.into();
3364 }
3365 let h = unsafe { &*handle };
3366 let _op = match h.guard.try_enter() {
3367 Some(op) => op,
3368 None => return NetError::ShuttingDown.into(),
3369 };
3370 let Some(filter_s) = (unsafe { c_str_to_string(filter_json) }) else {
3371 return NetError::InvalidUtf8.into();
3372 };
3373 let Some(scope_s) = (unsafe { c_str_to_string(scope_json) }) else {
3374 return NetError::InvalidUtf8.into();
3375 };
3376 let parsed_filter: CapabilityFilterJson = match serde_json::from_str(&filter_s) {
3377 Ok(v) => v,
3378 Err(_) => return NetError::InvalidJson.into(),
3379 };
3380 let parsed_scope: ScopeFilterJson = match serde_json::from_str(&scope_s) {
3381 Ok(v) => v,
3382 Err(_) => return NetError::InvalidJson.into(),
3383 };
3384 let filter = capability_filter_from_json(parsed_filter);
3385 let owned = scope_filter_from_json(parsed_scope);
3386 let ids = with_scope_filter(&owned, |sf| {
3387 h.inner.find_nodes_by_filter_scoped(&filter, sf)
3388 });
3389 write_json_out(&ids, out_json, out_len)
3390}
3391
3392#[derive(serde::Deserialize)]
3406struct CapabilityRequirementJson {
3407 #[serde(default)]
3408 filter: CapabilityFilterJson,
3409 #[serde(default)]
3410 prefer_more_memory: f32,
3411 #[serde(default)]
3412 prefer_more_vram: f32,
3413 #[serde(default)]
3414 prefer_faster_inference: f32,
3415 #[serde(default)]
3416 prefer_loaded_models: f32,
3417}
3418
3419fn capability_requirement_from_json(
3420 j: CapabilityRequirementJson,
3421) -> crate::adapter::net::behavior::capability::CapabilityRequirement {
3422 crate::adapter::net::behavior::capability::CapabilityRequirement::from_filter(
3423 capability_filter_from_json(j.filter),
3424 )
3425 .prefer_memory(j.prefer_more_memory)
3426 .prefer_vram(j.prefer_more_vram)
3427 .prefer_speed(j.prefer_faster_inference)
3428 .prefer_loaded(j.prefer_loaded_models)
3429}
3430
3431#[unsafe(no_mangle)]
3441pub unsafe extern "C" fn net_mesh_find_best_node(
3442 handle: *mut MeshNodeHandle,
3443 requirement_json: *const c_char,
3444 out_node_id: *mut u64,
3445 out_has_match: *mut c_int,
3446) -> c_int {
3447 if handle.is_null()
3448 || requirement_json.is_null()
3449 || out_node_id.is_null()
3450 || out_has_match.is_null()
3451 {
3452 return NetError::NullPointer.into();
3453 }
3454 let h = unsafe { &*handle };
3455 let _op = match h.guard.try_enter() {
3456 Some(op) => op,
3457 None => return NetError::ShuttingDown.into(),
3458 };
3459 let Some(s) = (unsafe { c_str_to_string(requirement_json) }) else {
3460 return NetError::InvalidUtf8.into();
3461 };
3462 let parsed: CapabilityRequirementJson = match serde_json::from_str(&s) {
3463 Ok(v) => v,
3464 Err(_) => return NetError::InvalidJson.into(),
3465 };
3466 let req = capability_requirement_from_json(parsed);
3467 match h.inner.find_best_node(&req) {
3468 Some(node_id) => unsafe {
3469 *out_node_id = node_id;
3470 *out_has_match = 1;
3471 },
3472 None => unsafe {
3473 *out_has_match = 0;
3474 },
3475 }
3476 0
3477}
3478
3479#[unsafe(no_mangle)]
3488pub unsafe extern "C" fn net_mesh_find_best_node_scoped(
3489 handle: *mut MeshNodeHandle,
3490 requirement_json: *const c_char,
3491 scope_json: *const c_char,
3492 out_node_id: *mut u64,
3493 out_has_match: *mut c_int,
3494) -> c_int {
3495 if handle.is_null()
3496 || requirement_json.is_null()
3497 || scope_json.is_null()
3498 || out_node_id.is_null()
3499 || out_has_match.is_null()
3500 {
3501 return NetError::NullPointer.into();
3502 }
3503 let h = unsafe { &*handle };
3504 let _op = match h.guard.try_enter() {
3505 Some(op) => op,
3506 None => return NetError::ShuttingDown.into(),
3507 };
3508 let Some(req_s) = (unsafe { c_str_to_string(requirement_json) }) else {
3509 return NetError::InvalidUtf8.into();
3510 };
3511 let Some(scope_s) = (unsafe { c_str_to_string(scope_json) }) else {
3512 return NetError::InvalidUtf8.into();
3513 };
3514 let parsed_req: CapabilityRequirementJson = match serde_json::from_str(&req_s) {
3515 Ok(v) => v,
3516 Err(_) => return NetError::InvalidJson.into(),
3517 };
3518 let parsed_scope: ScopeFilterJson = match serde_json::from_str(&scope_s) {
3519 Ok(v) => v,
3520 Err(_) => return NetError::InvalidJson.into(),
3521 };
3522 let req = capability_requirement_from_json(parsed_req);
3523 let owned = scope_filter_from_json(parsed_scope);
3524 let result = with_scope_filter(&owned, |sf| h.inner.find_best_node_scoped(&req, sf));
3525 match result {
3526 Some(node_id) => unsafe {
3527 *out_node_id = node_id;
3528 *out_has_match = 1;
3529 },
3530 None => unsafe {
3531 *out_has_match = 0;
3532 },
3533 }
3534 0
3535}
3536
3537#[unsafe(no_mangle)]
3539pub unsafe extern "C" fn net_normalize_gpu_vendor(
3540 raw: *const c_char,
3541 out_json: *mut *mut c_char,
3542 out_len: *mut usize,
3543) -> c_int {
3544 if raw.is_null() || out_json.is_null() || out_len.is_null() {
3545 return NetError::NullPointer.into();
3546 }
3547 let Some(s) = (unsafe { c_str_to_string(raw) }) else {
3548 return NetError::InvalidUtf8.into();
3549 };
3550 let canonical = gpu_vendor_to_string_cap(parse_gpu_vendor_cap(&s));
3551 write_string_out(canonical.to_string(), out_json, out_len)
3552}
3553
3554#[cfg(test)]
3555mod tests {
3556 use super::*;
3557
3558 #[test]
3570 fn saturating_u16_cap_clamps_at_u16_max() {
3571 assert_eq!(saturating_u16_cap(0), 0);
3572 assert_eq!(saturating_u16_cap(42), 42);
3573 assert_eq!(saturating_u16_cap(u16::MAX as u32), u16::MAX);
3574 assert_eq!(saturating_u16_cap(u16::MAX as u32 + 1), u16::MAX);
3575 assert_eq!(saturating_u16_cap(u32::MAX), u16::MAX);
3576 }
3577
3578 #[test]
3587 fn parse_modality_cap_returns_none_on_unknown_strings() {
3588 for (s, expected) in [
3590 ("text", Modality::Text),
3591 ("Text", Modality::Text),
3592 ("TEXT", Modality::Text),
3593 ("image", Modality::Image),
3594 ("audio", Modality::Audio),
3595 ("video", Modality::Video),
3596 ("code", Modality::Code),
3597 ("embedding", Modality::Embedding),
3598 ("tool-use", Modality::ToolUse),
3599 ("tool_use", Modality::ToolUse),
3600 ("tooluse", Modality::ToolUse),
3601 ] {
3602 assert_eq!(
3603 parse_modality_cap(s),
3604 Some(expected),
3605 "known modality `{s}` must parse",
3606 );
3607 }
3608
3609 for s in ["audoi", "imageX", "vidoe", "embeding", "garbage", ""] {
3611 assert_eq!(
3612 parse_modality_cap(s),
3613 None,
3614 "unknown modality `{s}` must return None — pre-fix this \
3615 fell back to Modality::Text, advertising a capability \
3616 the node didn't actually have",
3617 );
3618 }
3619 }
3620
3621 #[test]
3631 fn gpu_info_from_json_saturates_fp16_tflops_to_u16_max() {
3632 let g = GpuJson {
3635 vendor: None,
3636 model: "test".to_string(),
3637 vram_gb: 0,
3638 compute_units: None,
3639 tensor_cores: None,
3640 fp16_tflops_x10: Some(1_000_000_000u32),
3641 };
3642 let info = gpu_info_from_json(g);
3643 assert_eq!(
3647 info.fp16_tflops_x10,
3648 u16::MAX as u32,
3649 "fp16_tflops_x10 must saturate at u16::MAX (65535) instead of \
3650 losing precision through the f32 round-trip; got {}",
3651 info.fp16_tflops_x10,
3652 );
3653
3654 let g_small = GpuJson {
3656 vendor: None,
3657 model: "test".to_string(),
3658 vram_gb: 0,
3659 compute_units: None,
3660 tensor_cores: None,
3661 fp16_tflops_x10: Some(425), };
3663 let info_small = gpu_info_from_json(g_small);
3664 assert_eq!(
3665 info_small.fp16_tflops_x10, 425,
3666 "small fp16_tflops_x10 must round-trip exactly"
3667 );
3668 }
3669
3670 #[test]
3683 fn alloc_bytes_round_trip_across_sizes() {
3684 for size in [0usize, 1, 15, 16, 17, 32, 64, 1024, 8192] {
3685 let src: Vec<u8> = (0..size).map(|i| (i as u8).wrapping_mul(37)).collect();
3686 let mut ptr: *mut u8 = std::ptr::null_mut();
3687 let mut len: usize = 0;
3688 let rc = alloc_bytes(&src, &mut ptr as *mut _, &mut len as *mut _);
3689 assert_eq!(rc, 0);
3690 assert_eq!(len, size);
3691 if size == 0 {
3692 assert!(ptr.is_null());
3693 } else {
3694 assert!(!ptr.is_null());
3695 let observed = unsafe { std::slice::from_raw_parts(ptr, len) };
3696 assert_eq!(observed, &src[..]);
3697 }
3698 unsafe { net_free_bytes(ptr, len) };
3701 }
3702 }
3703
3704 #[test]
3705 fn net_free_bytes_null_and_zero_len_are_noops() {
3706 unsafe { net_free_bytes(std::ptr::null_mut(), 0) };
3708 unsafe { net_free_bytes(std::ptr::null_mut(), 42) };
3709 let mut sentinel: u8 = 0;
3712 unsafe { net_free_bytes(&mut sentinel as *mut u8, 0) };
3713 }
3714
3715 #[test]
3727 fn net_free_bytes_does_not_panic_on_oversized_len() {
3728 let mut sentinel: u8 = 0;
3736 let ptr = &mut sentinel as *mut u8;
3737 unsafe { net_free_bytes(ptr, usize::MAX) };
3740 assert_eq!(sentinel, 0, "sentinel must not have been written through");
3743 }
3744
3745 #[test]
3754 fn net_mesh_shutdown_runs_even_with_outstanding_arc_refs() {
3755 let cfg = serde_json::json!({
3756 "bind_addr": "127.0.0.1:0",
3757 "psk_hex": "0".repeat(64),
3758 });
3759 let cfg_c = CString::new(cfg.to_string()).unwrap();
3760 let mut out: *mut MeshNodeHandle = std::ptr::null_mut();
3761 let rc = unsafe { net_mesh_new(cfg_c.as_ptr(), &mut out) };
3762 assert_eq!(rc, 0, "net_mesh_new failed: {rc}");
3763 assert!(!out.is_null());
3764
3765 let inner_clone = {
3768 let h = unsafe { &*out };
3769 Arc::clone(&h.inner)
3770 };
3771 assert!(Arc::strong_count(&inner_clone) >= 2);
3772 assert!(!inner_clone.is_shutdown());
3773
3774 let rc = unsafe { net_mesh_shutdown(out) };
3775 assert_eq!(rc, 0, "net_mesh_shutdown returned {rc}");
3776 assert!(
3777 inner_clone.is_shutdown(),
3778 "shutdown flag must be set even when extra Arc refs are outstanding"
3779 );
3780
3781 drop(inner_clone);
3782 unsafe { net_mesh_free(out) };
3786 }
3787
3788 #[test]
3800 fn handles_match_rejects_stream_node_mismatch() {
3801 fn make_node_handle() -> *mut MeshNodeHandle {
3802 let cfg = serde_json::json!({
3803 "bind_addr": "127.0.0.1:0",
3804 "psk_hex": "0".repeat(64),
3805 });
3806 let cfg_c = CString::new(cfg.to_string()).unwrap();
3807 let mut out: *mut MeshNodeHandle = std::ptr::null_mut();
3808 let rc = unsafe { net_mesh_new(cfg_c.as_ptr(), &mut out) };
3809 assert_eq!(rc, 0);
3810 assert!(!out.is_null());
3811 out
3812 }
3813
3814 let nh_a = make_node_handle();
3815 let nh_b = make_node_handle();
3816
3817 let sh_a = {
3825 let h = unsafe { &*nh_a };
3826 let node_clone: Arc<MeshNode> = Arc::clone(&h.inner);
3827 MeshStreamHandle {
3828 stream: ManuallyDrop::new(CoreStream {
3829 peer_node_id: 0xDEAD,
3830 stream_id: 1,
3831 epoch: 0,
3832 config: StreamConfig::new(),
3833 }),
3834 _node: ManuallyDrop::new(node_clone),
3835 guard: HandleGuard::new(),
3836 }
3837 };
3838
3839 assert!(
3841 handles_match(&sh_a, unsafe { &*nh_a }),
3842 "stream from node_a + node_a handle must match"
3843 );
3844 assert!(
3846 !handles_match(&sh_a, unsafe { &*nh_b }),
3847 "stream from node_a + node_b handle must be rejected (#19)"
3848 );
3849
3850 unsafe {
3859 let mut sh_a = sh_a;
3860 let _ = ManuallyDrop::take(&mut sh_a.stream);
3861 let _ = ManuallyDrop::take(&mut sh_a._node);
3862 }
3863 unsafe { net_mesh_free(nh_a) };
3864 unsafe { net_mesh_free(nh_b) };
3865 }
3866
3867 #[test]
3874 fn net_mesh_free_is_idempotent() {
3875 let cfg = serde_json::json!({
3876 "bind_addr": "127.0.0.1:0",
3877 "psk_hex": "0".repeat(64),
3878 });
3879 let cfg_c = CString::new(cfg.to_string()).unwrap();
3880 let mut nh: *mut MeshNodeHandle = std::ptr::null_mut();
3881 assert_eq!(unsafe { net_mesh_new(cfg_c.as_ptr(), &mut nh) }, 0);
3882 assert!(!nh.is_null());
3883
3884 unsafe { net_mesh_free(nh) };
3885 unsafe { net_mesh_free(nh) };
3889 }
3890
3891 #[test]
3895 fn net_identity_free_is_idempotent() {
3896 let mut h: *mut IdentityHandle = std::ptr::null_mut();
3897 assert_eq!(unsafe { net_identity_generate(&mut h) }, 0);
3898 assert!(!h.is_null());
3899
3900 unsafe { net_identity_free(h) };
3901 unsafe { net_identity_free(h) };
3903 }
3904
3905 #[test]
3917 fn net_mesh_free_waits_for_inflight_op() {
3918 use std::sync::atomic::{AtomicBool, Ordering};
3919 use std::time::{Duration, Instant};
3920
3921 let cfg = serde_json::json!({
3922 "bind_addr": "127.0.0.1:0",
3923 "psk_hex": "0".repeat(64),
3924 });
3925 let cfg_c = CString::new(cfg.to_string()).unwrap();
3926 let mut nh: *mut MeshNodeHandle = std::ptr::null_mut();
3927 assert_eq!(unsafe { net_mesh_new(cfg_c.as_ptr(), &mut nh) }, 0);
3928 assert!(!nh.is_null());
3929
3930 let nh_addr = nh as usize;
3933 let started = Arc::new(AtomicBool::new(false));
3934 let release = Arc::new(AtomicBool::new(false));
3935 let started_w = started.clone();
3936 let release_w = release.clone();
3937
3938 let worker = std::thread::spawn(move || {
3939 let h = unsafe { &*(nh_addr as *mut MeshNodeHandle) };
3940 let op = h.guard.try_enter().expect("entry must succeed pre-free");
3944 started_w.store(true, Ordering::SeqCst);
3945 while !release_w.load(Ordering::SeqCst) {
3946 std::thread::sleep(Duration::from_millis(1));
3947 }
3948 drop(op);
3949 });
3950
3951 while !started.load(Ordering::SeqCst) {
3953 std::thread::yield_now();
3954 }
3955
3956 let release_clone = release.clone();
3959 std::thread::spawn(move || {
3960 std::thread::sleep(Duration::from_millis(50));
3961 release_clone.store(true, Ordering::SeqCst);
3962 });
3963
3964 let t0 = Instant::now();
3966 unsafe { net_mesh_free(nh) };
3967 let elapsed = t0.elapsed();
3968 assert!(
3969 elapsed >= Duration::from_millis(40),
3970 "net_mesh_free returned in {:?} — pre-fix it would have proceeded \
3971 immediately and the worker's subsequent op would UAF",
3972 elapsed,
3973 );
3974 worker.join().unwrap();
3975 }
3976
3977 #[test]
3984 fn net_mesh_stream_stats_returns_shutting_down_after_free() {
3985 let cfg = serde_json::json!({
3986 "bind_addr": "127.0.0.1:0",
3987 "psk_hex": "0".repeat(64),
3988 });
3989 let cfg_c = CString::new(cfg.to_string()).unwrap();
3990 let mut nh: *mut MeshNodeHandle = std::ptr::null_mut();
3991 assert_eq!(unsafe { net_mesh_new(cfg_c.as_ptr(), &mut nh) }, 0);
3992 assert!(!nh.is_null());
3993
3994 unsafe { net_mesh_free(nh) };
3997
3998 let mut out_json: *mut c_char = std::ptr::null_mut();
3999 let mut out_len: usize = 0;
4000 let rc = unsafe { net_mesh_stream_stats(nh, 0xDEAD, 1, &mut out_json, &mut out_len) };
4001 assert_eq!(
4002 rc,
4003 NetError::ShuttingDown as c_int,
4004 "post-free stream_stats must surface ShuttingDown (got {rc})",
4005 );
4006 assert!(
4007 out_json.is_null(),
4008 "no payload may be written after the guard fires",
4009 );
4010 }
4011
4012 #[test]
4017 fn net_identity_issue_token_returns_shutting_down_after_free() {
4018 let mut signer: *mut IdentityHandle = std::ptr::null_mut();
4019 assert_eq!(unsafe { net_identity_generate(&mut signer) }, 0);
4020 assert!(!signer.is_null());
4021 unsafe { net_identity_free(signer) };
4022
4023 let subject = [0u8; 32];
4026 let scope = CString::new("[\"publish\"]").unwrap();
4027 let channel = CString::new("test-channel").unwrap();
4028 let mut out_token: *mut u8 = std::ptr::null_mut();
4029 let mut out_token_len: usize = 0;
4030 let rc = unsafe {
4031 net_identity_issue_token(
4032 signer,
4033 subject.as_ptr(),
4034 subject.len(),
4035 scope.as_ptr(),
4036 channel.as_ptr(),
4037 60,
4038 0,
4039 &mut out_token,
4040 &mut out_token_len,
4041 )
4042 };
4043 assert_eq!(
4044 rc,
4045 NetError::ShuttingDown as c_int,
4046 "post-free issue_token must surface ShuttingDown (got {rc})",
4047 );
4048 assert!(out_token.is_null(), "no token bytes may be allocated");
4049 }
4050
4051 #[test]
4057 fn net_delegate_token_returns_shutting_down_after_free() {
4058 let mut signer: *mut IdentityHandle = std::ptr::null_mut();
4059 assert_eq!(unsafe { net_identity_generate(&mut signer) }, 0);
4060 assert!(!signer.is_null());
4061
4062 let subject = [0u8; 32];
4064 let scope = CString::new("[\"publish\",\"delegate\"]").unwrap();
4065 let channel = CString::new("test-channel").unwrap();
4066 let mut parent_bytes: *mut u8 = std::ptr::null_mut();
4067 let mut parent_len: usize = 0;
4068 assert_eq!(
4069 unsafe {
4070 net_identity_issue_token(
4071 signer,
4072 subject.as_ptr(),
4073 subject.len(),
4074 scope.as_ptr(),
4075 channel.as_ptr(),
4076 60,
4077 1,
4078 &mut parent_bytes,
4079 &mut parent_len,
4080 )
4081 },
4082 0,
4083 );
4084 assert!(!parent_bytes.is_null());
4085
4086 unsafe { net_identity_free(signer) };
4088
4089 let new_subject = [1u8; 32];
4090 let restricted = CString::new("[\"publish\"]").unwrap();
4091 let mut child_bytes: *mut u8 = std::ptr::null_mut();
4092 let mut child_len: usize = 0;
4093 let rc = unsafe {
4094 net_delegate_token(
4095 signer,
4096 parent_bytes,
4097 parent_len,
4098 new_subject.as_ptr(),
4099 new_subject.len(),
4100 restricted.as_ptr(),
4101 &mut child_bytes,
4102 &mut child_len,
4103 )
4104 };
4105 assert_eq!(
4106 rc,
4107 NetError::ShuttingDown as c_int,
4108 "post-free delegate_token must surface ShuttingDown (got {rc})",
4109 );
4110 assert!(child_bytes.is_null(), "no child token may be allocated");
4111
4112 unsafe { net_free_bytes(parent_bytes, parent_len) };
4114 }
4115
4116 #[test]
4117 fn hardware_from_json_saturates_overflow_cpu_fields() {
4118 let h = HardwareJson {
4121 cpu_cores: Some(70_000),
4122 cpu_threads: Some(200_000),
4123 memory_gb: None,
4124 gpu: None,
4125 additional_gpus: Vec::new(),
4126 storage_gb: None,
4127 network_gbps: None,
4128 accelerators: Vec::new(),
4129 };
4130 let hw = hardware_from_json(h);
4131 assert_eq!(hw.cpu_cores, u16::MAX);
4132 assert_eq!(hw.cpu_threads, u16::MAX);
4133 }
4134
4135 #[test]
4142 fn token_entry_points_reject_oversize_len() {
4143 let invalid_json: c_int = NetError::InvalidJson.into();
4144 let mut sentinel: u8 = 0;
4145 let token = &mut sentinel as *mut u8 as *const u8;
4146
4147 let mut out_json: *mut c_char = std::ptr::null_mut();
4148 let mut out_len: usize = 0;
4149 assert_eq!(
4150 unsafe { net_parse_token(token, usize::MAX, &mut out_json, &mut out_len) },
4151 invalid_json,
4152 );
4153 assert!(out_json.is_null());
4154
4155 let mut out_ok: c_int = -42;
4156 assert_eq!(
4157 unsafe { net_verify_token(token, usize::MAX, &mut out_ok) },
4158 invalid_json,
4159 );
4160
4161 let mut out_expired: c_int = -42;
4162 assert_eq!(
4163 unsafe { net_token_is_expired(token, usize::MAX, &mut out_expired) },
4164 invalid_json,
4165 );
4166
4167 assert_eq!(
4168 sentinel, 0,
4169 "sentinel must not be touched: the length guard fires before any deref"
4170 );
4171 }
4172}
4173
4174#[cfg(all(test, not(feature = "nat-traversal")))]
4175mod nat_traversal_stub_tests {
4176 use super::*;
4193 use std::ptr;
4194
4195 #[test]
4196 fn nat_type_stub_returns_unsupported() {
4197 let mut out_str: *mut c_char = ptr::null_mut();
4198 let mut out_len: usize = 0;
4199 let code = unsafe { net_mesh_nat_type(ptr::null_mut(), &mut out_str, &mut out_len) };
4202 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
4203 }
4204
4205 #[test]
4206 fn reflex_addr_stub_returns_unsupported() {
4207 let mut out_str: *mut c_char = ptr::null_mut();
4208 let mut out_len: usize = 0;
4209 let code = unsafe { net_mesh_reflex_addr(ptr::null_mut(), &mut out_str, &mut out_len) };
4211 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
4212 }
4213
4214 #[test]
4215 fn peer_nat_type_stub_returns_unsupported() {
4216 let mut out_str: *mut c_char = ptr::null_mut();
4217 let mut out_len: usize = 0;
4218 let code =
4220 unsafe { net_mesh_peer_nat_type(ptr::null_mut(), 0, &mut out_str, &mut out_len) };
4221 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
4222 }
4223
4224 #[test]
4225 fn probe_reflex_stub_returns_unsupported() {
4226 let mut out_str: *mut c_char = ptr::null_mut();
4227 let mut out_len: usize = 0;
4228 let code = unsafe { net_mesh_probe_reflex(ptr::null_mut(), 0, &mut out_str, &mut out_len) };
4230 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
4231 }
4232
4233 #[test]
4234 fn reclassify_nat_stub_returns_unsupported() {
4235 let code = unsafe { net_mesh_reclassify_nat(ptr::null_mut()) };
4237 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
4238 }
4239
4240 #[test]
4241 fn traversal_stats_stub_returns_unsupported() {
4242 let mut a: u64 = 0;
4243 let mut b: u64 = 0;
4244 let mut c: u64 = 0;
4245 let code = unsafe { net_mesh_traversal_stats(ptr::null_mut(), &mut a, &mut b, &mut c) };
4247 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
4248 }
4249
4250 #[test]
4251 fn connect_direct_stub_returns_unsupported() {
4252 let code = unsafe { net_mesh_connect_direct(ptr::null_mut(), 0, ptr::null(), 0) };
4254 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
4255 }
4256
4257 #[test]
4258 fn set_reflex_override_stub_returns_unsupported() {
4259 let code = unsafe { net_mesh_set_reflex_override(ptr::null_mut(), ptr::null()) };
4261 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
4262 }
4263
4264 #[test]
4265 fn clear_reflex_override_stub_returns_unsupported() {
4266 let code = unsafe { net_mesh_clear_reflex_override(ptr::null_mut()) };
4268 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
4269 }
4270
4271 #[test]
4277 fn unsupported_code_is_stable() {
4278 assert_eq!(NET_ERR_TRAVERSAL_UNSUPPORTED, -137);
4279 }
4280
4281 #[test]
4285 fn capability_set_from_go_marshal_preserves_gpu_vendor() {
4286 let json = r#"{"hardware":{"cpu_cores":16,"memory_gb":64,"gpu":{"vendor":"nvidia","model":"h100","vram_gb":80}},"tags":["gpu"]}"#;
4287 let parsed: CapabilitySetJson = serde_json::from_str(json).expect("JSON should parse");
4288 let caps = capability_set_from_json(parsed);
4289 let views = caps.views();
4293 assert_eq!(
4294 views.hardware().gpu_vendor(),
4295 Some(super::GpuVendor::Nvidia),
4296 "vendor lost in conversion"
4297 );
4298 assert_eq!(views.hardware().memory_gb, 64);
4299 assert_eq!(views.hardware().total_vram_gb(), 80);
4300 assert!(caps.has_tag("gpu"));
4301 }
4302
4303 #[test]
4312 fn collect_payloads_rejects_null_entry_with_nonzero_length() {
4313 let buf_a = b"hello".as_slice();
4314 let buf_b = b"world".as_slice();
4315 let ptrs: [*const u8; 3] = [buf_a.as_ptr(), std::ptr::null(), buf_b.as_ptr()];
4316 let lens: [usize; 3] = [buf_a.len(), 4, buf_b.len()];
4317
4318 let result = unsafe { collect_payloads(ptrs.as_ptr(), lens.as_ptr(), 3) };
4319 assert!(
4320 result.is_none(),
4321 "null entry with non-zero length must reject the whole batch"
4322 );
4323 }
4324
4325 #[test]
4326 fn collect_payloads_allows_null_entry_with_zero_length() {
4327 let buf_a = b"hello".as_slice();
4328 let ptrs: [*const u8; 2] = [buf_a.as_ptr(), std::ptr::null()];
4329 let lens: [usize; 2] = [buf_a.len(), 0];
4330
4331 let result = unsafe { collect_payloads(ptrs.as_ptr(), lens.as_ptr(), 2) }
4332 .expect("zero-length null is treated as empty payload");
4333 assert_eq!(result.len(), 2);
4334 assert_eq!(&result[0][..], b"hello");
4335 assert!(result[1].is_empty());
4336 }
4337
4338 #[test]
4339 fn collect_payloads_happy_path() {
4340 let buf_a = b"abc".as_slice();
4341 let buf_b = b"defg".as_slice();
4342 let ptrs: [*const u8; 2] = [buf_a.as_ptr(), buf_b.as_ptr()];
4343 let lens: [usize; 2] = [buf_a.len(), buf_b.len()];
4344
4345 let result = unsafe { collect_payloads(ptrs.as_ptr(), lens.as_ptr(), 2) }
4346 .expect("non-null entries should succeed");
4347 assert_eq!(result.len(), 2);
4348 assert_eq!(&result[0][..], b"abc");
4349 assert_eq!(&result[1][..], b"defg");
4350 }
4351}