1use crate::constants::{
25 BLE_FRAGMENT_TIMEOUT_SECS, BLE_MAX_FRAGMENT_ASSEMBLIES, BLE_MAX_FRAGMENT_COUNT,
26 BLE_MAX_FRAGMENT_SIZE, DEFAULT_MAX_MESSAGE_SIZE, FRAGMENT_HEADER_FIXED, FRAGMENT_MAGIC,
27 FRAGMENT_VERSION, MAX_REASONABLE_BLE_PAYLOAD,
28};
29use crate::{Result, SharedCallback, Transport, TransportMetrics, TransportStatus, TransportType};
30use offline_protocol_core::{Message, MutexExt};
31use offline_protocol_sealed::ACK_FOR_KEY;
32use std::collections::{HashMap, VecDeque};
33use std::convert::TryInto;
34use std::sync::atomic::{AtomicU64, Ordering};
35use std::sync::{Arc, Mutex};
36use std::time::{Duration as StdDuration, SystemTime};
37
38#[derive(Debug, Clone)]
40#[non_exhaustive]
41pub struct FragmentEvictionInfo {
42 pub message_id: String,
44 pub completion_percent: u8,
46}
47
48pub type FragmentEvictionCallback = Arc<dyn Fn(FragmentEvictionInfo) + Send + Sync>;
50
51#[derive(Debug, Clone)]
53pub struct PeerDevice {
54 pub device_id: String,
56 pub address: String,
58 pub rssi: i16,
60 pub last_seen: std::time::SystemTime,
62 pub connected: bool,
64}
65
66#[derive(Debug, Clone)]
67struct DecodedFragment {
68 message_id: String,
69 fragment_index: u16,
70 total_fragments: u16,
71 data: Vec<u8>,
72}
73
74#[derive(Debug)]
76struct FragmentAssembly {
77 total_fragments: u16,
79 fragments: HashMap<u16, Vec<u8>>,
81 started_at: SystemTime,
84 last_seen: SystemTime,
91 buffered_bytes: usize,
97}
98
99impl FragmentAssembly {
100 fn completion_ratio(&self) -> f32 {
102 if self.total_fragments == 0 {
103 return 0.0;
104 }
105 self.fragments.len() as f32 / self.total_fragments as f32
106 }
107
108 fn eviction_priority(&self, now: SystemTime) -> f32 {
111 let completion = self.completion_ratio();
112
113 let age_secs = now
115 .duration_since(self.started_at)
116 .unwrap_or(StdDuration::from_secs(0))
117 .as_secs_f32();
118 let age_penalty = (age_secs / 60.0).min(1.0) * 0.2; completion - age_penalty
123 }
124}
125
126pub struct BleTransport {
131 device_id: String,
133 status: Arc<Mutex<TransportStatus>>,
135 peers: Arc<Mutex<HashMap<String, PeerDevice>>>,
137 receive_queue: Arc<Mutex<VecDeque<Message>>>,
139 send_queue: Arc<Mutex<VecDeque<(String, Message)>>>,
141 #[allow(clippy::type_complexity)]
143 pending_fragments: Arc<Mutex<VecDeque<(String, Vec<u8>)>>>,
144 metrics: Arc<Mutex<TransportMetrics>>,
153 platform_handle: Arc<Mutex<Option<usize>>>,
155 fragment_buffers: Arc<Mutex<HashMap<String, FragmentAssembly>>>,
157 peer_mtus: Arc<Mutex<HashMap<String, usize>>>,
177 undersized_mtu_reports: Arc<AtomicU64>,
189 fragment_fallback_count: Arc<AtomicU64>,
218 recipient_not_among_peers_count: Arc<AtomicU64>,
233 on_fragments_available: SharedCallback,
237 eviction_callback: Arc<Mutex<Option<FragmentEvictionCallback>>>,
241}
242
243fn enqueue_send(queue: &mut VecDeque<(String, Message)>, entry: (String, Message)) {
255 let is_ack = |message: &Message| message.metadata.contains_key(ACK_FOR_KEY);
256 if is_ack(&entry.1) {
257 let at = queue
258 .iter()
259 .position(|(_, queued)| !is_ack(queued))
260 .unwrap_or(queue.len());
261 queue.insert(at, entry);
262 } else {
263 queue.push_back(entry);
264 }
265}
266
267impl BleTransport {
268 pub fn new(device_id: impl Into<String>) -> Self {
270 Self {
271 device_id: device_id.into(),
272 status: Arc::new(Mutex::new(TransportStatus::Unavailable)),
273 peers: Arc::new(Mutex::new(HashMap::new())),
274 receive_queue: Arc::new(Mutex::new(VecDeque::new())),
275 send_queue: Arc::new(Mutex::new(VecDeque::new())),
276 pending_fragments: Arc::new(Mutex::new(VecDeque::new())),
277 metrics: Arc::new(Mutex::new(TransportMetrics::default())),
278 platform_handle: Arc::new(Mutex::new(None)),
279 fragment_buffers: Arc::new(Mutex::new(HashMap::new())),
280 peer_mtus: Arc::new(Mutex::new(HashMap::new())),
281 undersized_mtu_reports: Arc::new(AtomicU64::new(0)),
282 fragment_fallback_count: Arc::new(AtomicU64::new(0)),
283 recipient_not_among_peers_count: Arc::new(AtomicU64::new(0)),
284 on_fragments_available: Arc::new(Mutex::new(None)),
285 eviction_callback: Arc::new(Mutex::new(None)),
286 }
287 }
288
289 pub fn set_on_fragments_available(&self, callback: Arc<dyn Fn() + Send + Sync>) {
294 *self.on_fragments_available.lock_or_recover() = Some(callback);
295 }
296
297 pub fn set_fragment_eviction_callback(&self, callback: Option<FragmentEvictionCallback>) {
300 *self.eviction_callback.lock_or_recover() = callback;
301 }
302
303 fn notify_fragments_available(&self) {
305 let callback = self.on_fragments_available.lock_or_recover().clone();
306 if let Some(cb) = callback {
307 cb();
308 }
309 }
310
311 pub fn peer_mtu(&self, peer_id: &str) -> usize {
314 self.peer_mtus
315 .lock_or_recover()
316 .get(peer_id)
317 .copied()
318 .unwrap_or(BLE_MAX_FRAGMENT_SIZE)
319 }
320
321 pub fn undersized_mtu_reports(&self) -> u64 {
330 self.undersized_mtu_reports.load(Ordering::Relaxed)
331 }
332
333 pub fn fragment_fallback_count(&self) -> u64 {
344 self.fragment_fallback_count.load(Ordering::Relaxed)
345 }
346
347 pub fn recipient_not_among_peers_count(&self) -> u64 {
355 self.recipient_not_among_peers_count.load(Ordering::Relaxed)
356 }
357
358 pub fn set_platform_handle(&self, handle: usize) {
362 crate::common::set_platform_handle(&self.platform_handle, handle);
363 }
364
365 pub fn platform_handle(&self) -> Option<usize> {
367 crate::common::platform_handle(&self.platform_handle)
368 }
369
370 pub fn device_id(&self) -> &str {
372 &self.device_id
373 }
374
375 pub fn on_peer_discovered(&self, peer: PeerDevice) {
377 let mut peers = self.peers.lock_or_recover();
378 peers.insert(peer.device_id.clone(), peer);
379 }
380
381 pub fn on_peer_lost(&self, device_id: &str) {
387 self.peers.lock_or_recover().remove(device_id);
388 self.peer_mtus.lock_or_recover().remove(device_id);
389 }
390
391 pub fn on_message_received(&self, message: Message) {
393 crate::common::on_message_received(&self.receive_queue, message);
394 }
395
396 pub fn on_message_received_from(&self, message: Message, peer_id: String) {
407 crate::common::on_message_received_from(&self.receive_queue, message, peer_id);
408 }
409
410 pub fn get_peers(&self) -> Vec<PeerDevice> {
412 let peers = self.peers.lock_or_recover();
413 peers.values().cloned().collect()
414 }
415
416 pub fn get_peer(&self, device_id: &str) -> Option<PeerDevice> {
418 let peers = self.peers.lock_or_recover();
419 peers.get(device_id).cloned()
420 }
421
422 pub fn update_metrics(&self, metrics: TransportMetrics) {
424 *self.metrics.lock_or_recover() = metrics;
425 }
426
427 fn update_queue_metric(&self) {
428 let send_len = self.send_queue.lock_or_recover().len();
429 let fragment_len = self.pending_fragments.lock_or_recover().len();
430 let mut metrics = self.metrics.lock_or_recover();
431 metrics.queue_depth = send_len + fragment_len;
432 let heuristic_capacity = 50_f32;
433 metrics.congestion = ((metrics.queue_depth as f32) / heuristic_capacity).clamp(0.0, 1.0);
434 }
435
436 fn record_latency(&self, latency_ms: u128) {
437 let value = latency_ms.min(u128::from(u32::MAX)) as u32;
438 let mut metrics = self.metrics.lock_or_recover();
439 metrics.latency_ms = Some(match metrics.latency_ms {
440 Some(existing) => {
441 let ema = (existing as f32 * 0.7) + (value as f32 * 0.3);
442 ema as u32
443 }
444 None => value,
445 });
446 }
447
448 fn cleanup_fragment_buffers(&self) {
449 let mut buffers = self.fragment_buffers.lock_or_recover();
450 let now = SystemTime::now();
451 let mut expired = Vec::new();
452
453 for (message_id, assembly) in buffers.iter() {
454 if now
458 .duration_since(assembly.last_seen)
459 .unwrap_or_else(|_| StdDuration::from_secs(0))
460 > StdDuration::from_secs(BLE_FRAGMENT_TIMEOUT_SECS)
461 {
462 expired.push(message_id.clone());
463 }
464 }
465
466 for message_id in expired {
467 buffers.remove(&message_id);
469 tracing::debug!(
470 message_id = %message_id,
471 "Dropped expired BLE fragment assembly"
472 );
473 }
474 }
475
476 pub fn get_queue_depth(&self) -> usize {
478 let send_len = self.send_queue.lock_or_recover().len();
479 let fragment_len = self.pending_fragments.lock_or_recover().len();
480 send_len + fragment_len
481 }
482
483 pub fn dequeue_send(&self) -> Option<(String, Message)> {
485 let result = {
486 let mut queue = self.send_queue.lock_or_recover();
487 queue.pop_front()
488 };
489
490 if result.is_some() {
491 self.update_queue_metric();
492 }
493
494 result
495 }
496
497 pub fn has_pending_sends(&self) -> bool {
499 let queue = self.send_queue.lock_or_recover();
500 !queue.is_empty()
501 }
502
503 pub fn serialize_message(&self, message: &Message) -> Result<Vec<u8>> {
505 crate::common::serialize_message_with(message)
506 }
507
508 pub fn fragment_message(&self, message: &Message) -> Result<Vec<Vec<u8>>> {
540 self.fragment_message_for(message.recipient.as_str(), message)
541 }
542
543 pub fn fragment_message_for(&self, peer_id: &str, message: &Message) -> Result<Vec<Vec<u8>>> {
554 let message_bytes = self.serialize_message(message)?;
555
556 let recipient = peer_id;
557 let cached_mtu = self.peer_mtus.lock_or_recover().get(recipient).copied();
588 let mtu = match cached_mtu {
589 Some(mtu) => mtu,
590 None => {
591 let is_direct_peer = self.peers.lock_or_recover().contains_key(recipient);
592 if is_direct_peer {
593 self.fragment_fallback_count.fetch_add(1, Ordering::Relaxed);
594 #[cfg(debug_assertions)]
599 tracing::warn!(
600 peer = %recipient,
601 "ble: fragment_message found no MTU for a registered direct peer — \
602 MTU-before-discover ordering invariant regressed, or the \
603 recipient -> device_id keying contract broke"
604 );
605 } else {
606 #[cfg(debug_assertions)]
614 tracing::debug!(
615 peer = %recipient,
616 "ble: fragment_message for recipient no longer in direct-peer map — \
617 benign send/on_peer_lost race, not counted"
618 );
619 }
620 BLE_MAX_FRAGMENT_SIZE
621 }
622 };
623 tracing::trace!(
624 peer = %recipient,
625 mtu,
626 "ble: selected fragment payload size"
627 );
628 let message_id = message.id.as_str();
629 let message_id_bytes = message_id.as_bytes();
630 if message_id_bytes.len() > u8::MAX as usize {
631 return Err(crate::Error::Other("Message ID too long".to_string()));
632 }
633
634 let header_overhead = FRAGMENT_HEADER_FIXED + message_id_bytes.len();
636 if header_overhead >= mtu {
637 return Err(crate::Error::Other(
638 "MTU too small for fragment header".to_string(),
639 ));
640 }
641
642 let max_fragment_payload = mtu - header_overhead;
643 let total_fragments = message_bytes.len().div_ceil(max_fragment_payload);
644 if total_fragments == 0 {
645 return Err(crate::Error::Other("Empty message".to_string()));
646 }
647 if total_fragments > BLE_MAX_FRAGMENT_COUNT {
648 return Err(crate::Error::Other(
649 "Message would require too many BLE fragments".to_string(),
650 ));
651 }
652
653 if total_fragments > u16::MAX as usize {
654 return Err(crate::Error::Other(
655 "Message too large to fragment".to_string(),
656 ));
657 }
658
659 let mut fragments = Vec::with_capacity(total_fragments);
660 for (i, chunk) in message_bytes.chunks(max_fragment_payload).enumerate() {
661 let encoded =
662 encode_fragment(message_id_bytes, i as u16, total_fragments as u16, chunk)?;
663 fragments.push(encoded);
664 }
665
666 Ok(fragments)
667 }
668
669 pub fn process_fragment(&self, fragment_data: &[u8]) -> Result<Option<Message>> {
673 self.cleanup_fragment_buffers();
674
675 let fragment = decode_fragment(fragment_data)?;
677
678 if fragment.total_fragments as usize > BLE_MAX_FRAGMENT_COUNT {
683 return Err(crate::Error::Other(
684 "Fragment count exceeds maximum".to_string(),
685 ));
686 }
687
688 if fragment.fragment_index >= fragment.total_fragments {
698 return Err(crate::Error::Other(
699 "Fragment index out of range".to_string(),
700 ));
701 }
702
703 if fragment.total_fragments == 1 {
704 if fragment.data.len() > DEFAULT_MAX_MESSAGE_SIZE {
705 return Err(crate::Error::MessageTooLarge(
706 fragment.data.len(),
707 DEFAULT_MAX_MESSAGE_SIZE,
708 ));
709 }
710 return Ok(Some(self.deserialize_message(&fragment.data)?));
711 }
712
713 let mut completed_payload: Option<Vec<u8>> = None;
714 let mut assembly_started_at: Option<SystemTime> = None;
715 let mut eviction_info: Option<FragmentEvictionInfo> = None;
718
719 {
720 let mut buffers = self.fragment_buffers.lock_or_recover();
722 let now = SystemTime::now();
723
724 if !buffers.contains_key(&fragment.message_id)
725 && buffers.len() >= BLE_MAX_FRAGMENT_ASSEMBLIES
726 {
727 if let Some((evict_id, completion_percent)) = buffers
730 .iter()
731 .min_by(|(_, a), (_, b)| {
732 let priority_a = a.eviction_priority(now);
733 let priority_b = b.eviction_priority(now);
734 priority_a
735 .partial_cmp(&priority_b)
736 .unwrap_or(std::cmp::Ordering::Equal)
737 })
738 .map(|(id, assembly)| {
739 let pct = (assembly.completion_ratio() * 100.0)
740 .round()
741 .clamp(0.0, 100.0) as u8;
742 (id.clone(), pct)
743 })
744 {
745 tracing::debug!(
746 message_id = %evict_id,
747 completion_percent,
748 "Evicting fragment assembly to make room (priority-based)"
749 );
750
751 eviction_info = Some(FragmentEvictionInfo {
752 message_id: evict_id.clone(),
753 completion_percent,
754 });
755
756 buffers.remove(&evict_id);
757 }
758 }
759
760 let projected_bytes = {
765 let existing = buffers.get(&fragment.message_id);
766
767 if let Some(a) = existing {
769 if a.total_fragments != fragment.total_fragments {
770 return Err(crate::Error::Other(format!(
771 "Fragment count mismatch: expected {}, got {}",
772 a.total_fragments, fragment.total_fragments
773 )));
774 }
775 }
776
777 let prior_total = existing.map(|a| a.buffered_bytes).unwrap_or(0);
780 let prior_len = existing
781 .and_then(|a| a.fragments.get(&fragment.fragment_index))
782 .map(Vec::len)
783 .unwrap_or(0);
784 prior_total.saturating_sub(prior_len) + fragment.data.len()
785 };
786
787 if projected_bytes > DEFAULT_MAX_MESSAGE_SIZE {
793 buffers.remove(&fragment.message_id);
794 return Err(crate::Error::MessageTooLarge(
795 projected_bytes,
796 DEFAULT_MAX_MESSAGE_SIZE,
797 ));
798 }
799
800 let assembly = buffers
802 .entry(fragment.message_id.clone())
803 .or_insert_with(|| FragmentAssembly {
804 total_fragments: fragment.total_fragments,
805 fragments: HashMap::new(),
806 started_at: now,
807 last_seen: now,
808 buffered_bytes: 0,
809 });
810
811 assembly.buffered_bytes = projected_bytes;
812 assembly
813 .fragments
814 .insert(fragment.fragment_index, fragment.data);
815 assembly.last_seen = now;
825
826 if assembly.fragments.len() == assembly.total_fragments as usize {
828 let mut complete_data = Vec::new();
830 for i in 0..assembly.total_fragments {
831 if let Some(data) = assembly.fragments.get(&i) {
832 complete_data.extend_from_slice(data);
833 } else {
834 return Err(crate::Error::Other(format!("Missing fragment {}", i)));
835 }
836 }
837
838 assembly_started_at = Some(assembly.started_at);
839 buffers.remove(&fragment.message_id);
840 completed_payload = Some(complete_data);
841 }
842 }
843
844 if let Some(info) = eviction_info {
850 let cb = self.eviction_callback.lock_or_recover().clone();
851 if let Some(cb) = cb {
852 cb(info);
853 }
854 }
855
856 if let Some(payload) = completed_payload {
857 if payload.len() > DEFAULT_MAX_MESSAGE_SIZE {
858 return Err(crate::Error::MessageTooLarge(
859 payload.len(),
860 DEFAULT_MAX_MESSAGE_SIZE,
861 ));
862 }
863
864 let start = assembly_started_at.unwrap_or_else(SystemTime::now);
865 let latency = SystemTime::now()
866 .duration_since(start)
867 .unwrap_or_else(|_| StdDuration::from_millis(0))
868 .as_millis();
869 self.record_latency(latency);
870
871 let message = self.deserialize_message(&payload)?;
873 return Ok(Some(message));
874 }
875
876 Ok(None)
878 }
879
880 pub fn on_fragment_received_from(&self, fragment_data: Vec<u8>, peer_id: String) -> Result<()> {
891 match self.process_fragment(&fragment_data) {
892 Ok(Some(mut message)) => {
893 message.set_transport_peer_id(peer_id)?;
894 let msg_id = message.id.clone();
895 let mut queue = self.receive_queue.lock_or_recover();
896 queue.push_back(message);
897 tracing::debug!(
898 message_id = %msg_id,
899 "Complete message assembled from fragments (with peer identity)"
900 );
901 Ok(())
902 }
903 Ok(None) => {
904 tracing::debug!("Fragment received, more needed for complete message");
905 Ok(())
906 }
907 Err(e) => {
908 tracing::warn!(error = %e, "Error processing fragment, dropping bad fragment");
909 Ok(())
910 }
911 }
912 }
913
914 pub fn requeue_fragment(&self, recipient: &str, fragment_data: Vec<u8>) {
916 {
917 let mut pending = self.pending_fragments.lock_or_recover();
918 pending.push_front((recipient.to_string(), fragment_data));
919 }
920
921 self.update_queue_metric();
922 }
923}
924
925fn encode_fragment(
926 message_id: &[u8],
927 fragment_index: u16,
928 total_fragments: u16,
929 data: &[u8],
930) -> Result<Vec<u8>> {
931 if data.len() > u16::MAX as usize {
932 return Err(crate::Error::Other(
933 "Fragment payload too large".to_string(),
934 ));
935 }
936
937 let mut encoded = Vec::with_capacity(FRAGMENT_HEADER_FIXED + message_id.len() + data.len());
938 encoded.extend_from_slice(&FRAGMENT_MAGIC);
939 encoded.push(FRAGMENT_VERSION);
940 encoded.push(message_id.len() as u8);
941 encoded.extend_from_slice(message_id);
942 encoded.extend_from_slice(&fragment_index.to_le_bytes());
943 encoded.extend_from_slice(&total_fragments.to_le_bytes());
944 encoded.extend_from_slice(&(data.len() as u16).to_le_bytes());
945 encoded.extend_from_slice(data);
946 Ok(encoded)
947}
948
949fn decode_fragment(fragment_data: &[u8]) -> Result<DecodedFragment> {
950 if fragment_data.len() < FRAGMENT_HEADER_FIXED {
951 return Err(crate::Error::Other("Fragment too short".to_string()));
952 }
953
954 if fragment_data[0..2] != FRAGMENT_MAGIC {
955 return Err(crate::Error::Other("Invalid fragment magic".to_string()));
956 }
957
958 let version = fragment_data[2];
959 if version != FRAGMENT_VERSION {
960 return Err(crate::Error::Other(format!(
961 "Unsupported fragment version {}",
962 version
963 )));
964 }
965
966 let id_len = fragment_data[3] as usize;
967 let header_len = FRAGMENT_HEADER_FIXED + id_len;
968 if fragment_data.len() < header_len {
969 return Err(crate::Error::Other("Fragment truncated (id)".to_string()));
970 }
971
972 let mut offset = 4;
973 let message_id_bytes = &fragment_data[offset..offset + id_len];
974 offset += id_len;
975
976 let message_id = String::from_utf8(message_id_bytes.to_vec())
977 .map_err(|_| crate::Error::Other("Invalid UTF-8 in message ID".to_string()))?;
978
979 if fragment_data.len() < offset + 6 {
980 return Err(crate::Error::Other(
981 "Fragment truncated (header)".to_string(),
982 ));
983 }
984
985 let fragment_index = u16::from_le_bytes(
986 fragment_data[offset..offset + 2]
987 .try_into()
988 .map_err(|_| crate::Error::Other("Fragment truncated (index)".to_string()))?,
989 );
990 offset += 2;
991 let total_fragments = u16::from_le_bytes(
992 fragment_data[offset..offset + 2]
993 .try_into()
994 .map_err(|_| crate::Error::Other("Fragment truncated (total)".to_string()))?,
995 );
996 offset += 2;
997 let data_len = u16::from_le_bytes(
998 fragment_data[offset..offset + 2]
999 .try_into()
1000 .map_err(|_| crate::Error::Other("Fragment truncated (length)".to_string()))?,
1001 ) as usize;
1002 offset += 2;
1003
1004 if fragment_data.len() < offset + data_len {
1005 return Err(crate::Error::Other("Fragment truncated (data)".to_string()));
1006 }
1007
1008 let data = fragment_data[offset..offset + data_len].to_vec();
1009
1010 Ok(DecodedFragment {
1011 message_id,
1012 fragment_index,
1013 total_fragments,
1014 data,
1015 })
1016}
1017
1018impl Transport for BleTransport {
1019 fn as_any(&self) -> &dyn std::any::Any {
1020 self
1021 }
1022
1023 fn transport_type(&self) -> TransportType {
1024 TransportType::BLE
1025 }
1026
1027 fn status(&self) -> TransportStatus {
1028 *self.status.lock_or_recover()
1029 }
1030
1031 fn metrics(&self) -> TransportMetrics {
1032 self.metrics.lock_or_recover().clone()
1033 }
1034
1035 fn send(&self, message: &Message) -> Result<()> {
1036 let status = self.status();
1037
1038 if status != TransportStatus::Available {
1039 return Err(crate::Error::TransportNotAvailable(format!(
1040 "BLE transport is not available (status: {:?})",
1041 status
1042 )));
1043 }
1044
1045 let recipient = message.recipient.as_str().to_string();
1046
1047 {
1048 let peers = self.peers.lock_or_recover();
1049 if !peers.contains_key(&recipient) {
1050 if !peers.is_empty() {
1057 self.recipient_not_among_peers_count
1058 .fetch_add(1, Ordering::Relaxed);
1059 tracing::warn!(
1060 recipient = %recipient,
1061 connected_peers = peers.len(),
1062 "ble: send recipient is not among the connected BLE peers while \
1063 other peers are connected — likely a recipient↔device-id keying \
1064 mismatch; this send will fall back off BLE"
1065 );
1066 }
1067 return Err(crate::Error::PeerNotReachable(format!(
1068 "BLE: no connected peer for recipient {}",
1069 recipient
1070 )));
1071 }
1072 }
1073
1074 enqueue_send(
1075 &mut self.send_queue.lock_or_recover(),
1076 (recipient, message.clone()),
1077 );
1078
1079 self.update_queue_metric();
1080 self.notify_fragments_available();
1081
1082 Ok(())
1083 }
1084
1085 fn queued_message_ids(&self) -> Vec<String> {
1088 self.send_queue
1089 .lock_or_recover()
1090 .iter()
1091 .map(|(_, message)| message.id.as_str())
1092 .collect()
1093 }
1094
1095 fn send_to_peer(&self, peer_id: &str, message: &Message) -> Result<()> {
1101 let status = self.status();
1102
1103 if status != TransportStatus::Available {
1104 return Err(crate::Error::TransportNotAvailable(format!(
1105 "BLE transport is not available (status: {:?})",
1106 status
1107 )));
1108 }
1109
1110 {
1111 let peers = self.peers.lock_or_recover();
1112 if !peers.contains_key(peer_id) {
1113 return Err(crate::Error::PeerNotReachable(format!(
1114 "BLE: {} is not a connected peer",
1115 peer_id
1116 )));
1117 }
1118 }
1119
1120 enqueue_send(
1121 &mut self.send_queue.lock_or_recover(),
1122 (peer_id.to_string(), message.clone()),
1123 );
1124
1125 self.update_queue_metric();
1126 self.notify_fragments_available();
1127
1128 Ok(())
1129 }
1130
1131 fn connected_peers(&self) -> Vec<crate::PeerLink> {
1140 if self.status() != TransportStatus::Available {
1141 return Vec::new();
1142 }
1143 self.peers
1144 .lock_or_recover()
1145 .values()
1146 .map(|peer| crate::PeerLink::with_rssi(peer.device_id.clone(), peer.rssi))
1147 .collect()
1148 }
1149
1150 fn receive(&self) -> Result<Option<Message>> {
1151 let mut queue = self.receive_queue.lock_or_recover();
1152 Ok(queue.pop_front())
1153 }
1154
1155 fn start(&self) -> Result<()> {
1156 *self.status.lock_or_recover() = TransportStatus::Available;
1159 Ok(())
1160 }
1161
1162 fn stop(&self) -> Result<()> {
1163 *self.status.lock_or_recover() = TransportStatus::Disconnected;
1171 self.peers.lock_or_recover().clear();
1182 self.peer_mtus.lock_or_recover().clear();
1183 self.fragment_buffers.lock_or_recover().clear();
1184 self.send_queue.lock_or_recover().clear();
1185 self.pending_fragments.lock_or_recover().clear();
1186 self.receive_queue.lock_or_recover().clear();
1187 self.update_queue_metric();
1188 Ok(())
1189 }
1190
1191 fn on_status_changed(&self, status: TransportStatus) {
1200 let previous = {
1201 let mut guard = self.status.lock_or_recover();
1202 let prev = *guard;
1203 *guard = status;
1204 prev
1205 };
1206
1207 if previous == TransportStatus::Available && status != TransportStatus::Available {
1208 self.peers.lock_or_recover().clear();
1209 self.peer_mtus.lock_or_recover().clear();
1210 self.fragment_buffers.lock_or_recover().clear();
1211 self.send_queue.lock_or_recover().clear();
1212 self.pending_fragments.lock_or_recover().clear();
1213 self.receive_queue.lock_or_recover().clear();
1214 self.update_queue_metric();
1215 }
1216 }
1217
1218 fn on_fragment_received(&self, fragment_data: Vec<u8>) -> Result<()> {
1222 match self.process_fragment(&fragment_data) {
1223 Ok(Some(message)) => {
1224 let mut queue = self.receive_queue.lock_or_recover();
1226 queue.push_back(message.clone());
1227 tracing::debug!(
1229 message_id = %message.id,
1230 "Complete message assembled from fragments"
1231 );
1232 Ok(())
1233 }
1234 Ok(None) => {
1235 tracing::debug!("Fragment received, more needed for complete message");
1237 Ok(())
1238 }
1239 Err(e) => {
1240 tracing::warn!(error = %e, "Error processing fragment, dropping bad fragment");
1242 Ok(())
1243 }
1244 }
1245 }
1246
1247 fn on_fragment_received_from(&self, fragment_data: Vec<u8>, peer_id: String) -> Result<()> {
1255 if peer_id.is_empty() {
1256 return self.on_fragment_received(fragment_data);
1257 }
1258 BleTransport::on_fragment_received_from(self, fragment_data, peer_id)
1259 }
1260
1261 fn get_next_fragment(&self) -> Result<Option<(String, Vec<u8>)>> {
1265 if let Some(fragment) = {
1267 let mut pending = self.pending_fragments.lock_or_recover();
1268 pending.pop_front()
1269 } {
1270 self.update_queue_metric();
1271 return Ok(Some(fragment));
1272 }
1273
1274 let maybe_message = {
1276 let mut queue = self.send_queue.lock_or_recover();
1277 queue.pop_front()
1278 };
1279
1280 let Some((recipient, message)) = maybe_message else {
1281 self.update_queue_metric();
1282 return Ok(None);
1283 };
1284
1285 let fragments = self.fragment_message_for(&recipient, &message)?;
1289
1290 if fragments.is_empty() {
1291 self.update_queue_metric();
1292 return Ok(None);
1293 }
1294
1295 {
1296 let mut pending = self.pending_fragments.lock_or_recover();
1297 for fragment in fragments {
1298 pending.push_back((recipient.clone(), fragment));
1299 }
1300 }
1301
1302 self.update_queue_metric();
1303
1304 let result = {
1305 let mut pending = self.pending_fragments.lock_or_recover();
1306 pending.pop_front()
1307 };
1308
1309 self.update_queue_metric();
1310
1311 Ok(result)
1312 }
1313
1314 fn set_on_messages_available(&self, callback: Arc<dyn Fn() + Send + Sync>) {
1319 self.set_on_fragments_available(callback);
1320 }
1321
1322 fn set_peer_mtu(&self, peer_id: &str, max_payload: usize) {
1347 #[cfg(debug_assertions)]
1354 {
1355 if !self.peers.lock_or_recover().contains_key(peer_id) {
1356 tracing::warn!(
1357 peer = %peer_id,
1358 max_payload,
1359 "ble: set_peer_mtu for unregistered peer — platform must call \
1360 on_peer_discovered before reporting MTU, otherwise the stored \
1361 entry will not be cleared on peer loss"
1362 );
1363 }
1364 }
1365 if max_payload < BLE_MAX_FRAGMENT_SIZE {
1366 self.undersized_mtu_reports.fetch_add(1, Ordering::Relaxed);
1367 tracing::warn!(
1368 peer = %peer_id,
1369 max_payload,
1370 floor = BLE_MAX_FRAGMENT_SIZE,
1371 "ble: undersized MTU report; dropping stored entry and falling back"
1372 );
1373 self.peer_mtus.lock_or_recover().remove(peer_id);
1374 return;
1375 }
1376 let clamped = max_payload.min(MAX_REASONABLE_BLE_PAYLOAD);
1377 tracing::debug!(
1378 peer = %peer_id,
1379 raw = max_payload,
1380 clamped,
1381 "ble: stored per-peer MTU"
1382 );
1383 self.peer_mtus
1384 .lock_or_recover()
1385 .insert(peer_id.to_string(), clamped);
1386 }
1387
1388 fn clear_peer_mtu(&self, peer_id: &str) {
1391 tracing::debug!(peer = %peer_id, "ble: cleared per-peer MTU");
1392 self.peer_mtus.lock_or_recover().remove(peer_id);
1393 }
1394}
1395
1396pub struct BleTransportBuilder {
1398 device_id: String,
1399}
1400
1401impl BleTransportBuilder {
1402 pub fn new(device_id: impl Into<String>) -> Self {
1404 Self {
1405 device_id: device_id.into(),
1406 }
1407 }
1408
1409 pub fn build(self) -> BleTransport {
1411 BleTransport::new(self.device_id)
1412 }
1413}
1414
1415#[cfg(test)]
1416mod tests {
1417 use super::*;
1418 use offline_protocol_core::{AppId, Message, MessagePriority, UserId, TTL};
1419
1420 fn peer_device(id: &str) -> PeerDevice {
1421 PeerDevice {
1422 device_id: id.to_string(),
1423 address: "AA:BB:CC:DD:EE:FF".to_string(),
1424 rssi: -60,
1425 last_seen: std::time::SystemTime::now(),
1426 connected: false,
1427 }
1428 }
1429
1430 fn small_message() -> Message {
1431 Message::new(
1432 UserId::new("alice").unwrap(),
1433 UserId::new("bob").unwrap(),
1434 AppId::new("app").unwrap(),
1435 "hi",
1436 )
1437 }
1438
1439 #[test]
1440 fn send_to_peer_targets_the_hop_and_sizes_by_its_mtu() {
1441 let transport = BleTransport::new("relay");
1442 transport.start().unwrap();
1443 transport.on_peer_discovered(peer_device("carol"));
1444 transport.set_peer_mtu("carol", 500);
1447
1448 let message = small_message(); transport.send_to_peer("carol", &message).unwrap();
1450
1451 let (target, _) = transport
1452 .get_next_fragment()
1453 .unwrap()
1454 .expect("fragment queued for the hop");
1455 assert_eq!(target, "carol");
1456 assert!(transport.get_next_fragment().unwrap().is_none());
1459 assert_eq!(transport.fragment_fallback_count(), 0);
1460 }
1461
1462 #[test]
1463 fn send_to_peer_refuses_an_unconnected_peer() {
1464 let transport = BleTransport::new("relay");
1465 transport.start().unwrap();
1466 transport.on_peer_discovered(peer_device("carol"));
1467
1468 let result = transport.send_to_peer("dave", &small_message());
1469 assert!(matches!(result, Err(crate::Error::PeerNotReachable(_))));
1470 }
1471
1472 #[test]
1473 fn connected_peers_matches_what_the_send_path_accepts() {
1474 let transport = BleTransport::new("relay");
1475 transport.on_peer_discovered(peer_device("carol"));
1476
1477 assert!(transport.connected_peers().is_empty());
1479
1480 transport.start().unwrap();
1481 let links = transport.connected_peers();
1482 assert_eq!(links.len(), 1);
1483 assert_eq!(links[0].peer_id, "carol");
1484 assert_eq!(links[0].rssi, Some(-60));
1485 assert!(transport
1487 .send_to_peer(&links[0].peer_id, &small_message())
1488 .is_ok());
1489
1490 transport.on_peer_lost("carol");
1491 assert!(transport.connected_peers().is_empty());
1492 }
1493
1494 #[test]
1495 fn test_ble_transport_creation() {
1496 let transport = BleTransport::new("test-device");
1497 assert_eq!(transport.device_id(), "test-device");
1498 assert_eq!(transport.status(), TransportStatus::Unavailable);
1499 }
1500
1501 #[test]
1502 fn test_ble_send_when_unavailable_fails() {
1503 let transport = BleTransport::new("test-device");
1504 let msg = small_message();
1505 let result = transport.send(&msg);
1506 assert!(result.is_err());
1507 assert!(matches!(
1508 result.unwrap_err(),
1509 crate::Error::TransportNotAvailable(_)
1510 ));
1511 }
1512
1513 #[test]
1514 fn test_ble_start_stop() {
1515 let transport = BleTransport::new("test-device");
1516 transport.start().unwrap();
1517 assert_eq!(transport.status(), TransportStatus::Available);
1518 transport.stop().unwrap();
1519 assert_eq!(transport.status(), TransportStatus::Disconnected);
1520 }
1521
1522 #[test]
1523 fn test_ble_recipient_not_among_peers_count_signals_keying_mismatch() {
1524 let transport = BleTransport::new("test-device");
1525 transport.start().unwrap();
1526
1527 assert!(matches!(
1530 transport.send(&small_message()).unwrap_err(),
1531 crate::Error::PeerNotReachable(_)
1532 ));
1533 assert_eq!(transport.recipient_not_among_peers_count(), 0);
1534
1535 transport.on_peer_discovered(peer_device("alice"));
1539 assert!(matches!(
1540 transport.send(&small_message()).unwrap_err(),
1541 crate::Error::PeerNotReachable(_)
1542 ));
1543 assert_eq!(transport.recipient_not_among_peers_count(), 1);
1544
1545 transport.on_peer_discovered(peer_device("bob"));
1547 transport.send(&small_message()).unwrap();
1548 assert_eq!(transport.recipient_not_among_peers_count(), 1);
1549 }
1550
1551 #[test]
1552 fn test_ble_on_status_changed() {
1553 let transport = BleTransport::new("test-device");
1554 transport.on_status_changed(TransportStatus::Available);
1555 assert_eq!(transport.status(), TransportStatus::Available);
1556 transport.on_status_changed(TransportStatus::Error);
1557 assert_eq!(transport.status(), TransportStatus::Error);
1558 }
1559
1560 #[test]
1561 fn test_ble_on_status_changed_drains_session_state() {
1562 let transport = BleTransport::new("test-device");
1563 transport.on_status_changed(TransportStatus::Available);
1564
1565 transport.on_peer_discovered(peer_device("bob"));
1566 transport.set_peer_mtu("bob", 244);
1567 transport.send(&small_message()).unwrap();
1568
1569 transport.fragment_buffers.lock().unwrap().insert(
1572 "msg-1".to_string(),
1573 FragmentAssembly {
1574 total_fragments: 2,
1575 fragments: HashMap::new(),
1576 started_at: SystemTime::now(),
1577 last_seen: SystemTime::now(),
1578 buffered_bytes: 0,
1579 },
1580 );
1581
1582 transport
1584 .undersized_mtu_reports
1585 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1586
1587 transport.on_status_changed(TransportStatus::Unavailable);
1589
1590 assert!(transport.peers.lock().unwrap().is_empty());
1591 assert!(transport.peer_mtus.lock().unwrap().is_empty());
1592 assert!(transport.fragment_buffers.lock().unwrap().is_empty());
1593 assert!(transport.send_queue.lock().unwrap().is_empty());
1594 assert!(transport.pending_fragments.lock().unwrap().is_empty());
1595 assert!(transport.receive_queue.lock().unwrap().is_empty());
1596 assert_eq!(transport.metrics.lock().unwrap().queue_depth, 0);
1597
1598 assert_eq!(
1600 transport
1601 .undersized_mtu_reports
1602 .load(std::sync::atomic::Ordering::Relaxed),
1603 1
1604 );
1605 }
1606
1607 #[test]
1611 fn queued_message_ids_lists_a_message_until_its_first_fragment_is_pulled() {
1612 let transport = BleTransport::new("test-device");
1613 transport.on_status_changed(TransportStatus::Available);
1614 transport.on_peer_discovered(peer_device("bob"));
1615 let message = small_message();
1616 transport.send(&message).unwrap();
1617
1618 assert_eq!(transport.queued_message_ids(), vec![message.id.as_str()]);
1619 assert!(transport.get_next_fragment().unwrap().is_some());
1620 assert!(transport.queued_message_ids().is_empty());
1621 }
1622
1623 #[test]
1624 fn test_ble_on_status_changed_available_to_available_preserves_state() {
1625 let transport = BleTransport::new("test-device");
1626 transport.on_status_changed(TransportStatus::Available);
1627
1628 transport.on_peer_discovered(peer_device("bob"));
1629 transport.set_peer_mtu("bob", 244);
1630 transport.send(&small_message()).unwrap();
1631
1632 transport.on_status_changed(TransportStatus::Available);
1634
1635 assert_eq!(transport.peers.lock().unwrap().len(), 1);
1636 assert_eq!(transport.peer_mtus.lock().unwrap().len(), 1);
1637 assert!(!transport.send_queue.lock().unwrap().is_empty());
1638 }
1639
1640 #[test]
1641 fn test_ble_on_status_changed_non_available_to_non_available_no_drain() {
1642 let transport = BleTransport::new("test-device");
1643
1644 transport
1646 .peers
1647 .lock()
1648 .unwrap()
1649 .insert("alice".to_string(), peer_device("alice"));
1650
1651 transport.on_status_changed(TransportStatus::Error);
1654 assert_eq!(transport.peers.lock().unwrap().len(), 1);
1655 }
1656
1657 #[test]
1658 fn test_ble_peer_mtu_defaults_to_fallback() {
1659 let transport = BleTransport::new("test-device");
1660 assert_eq!(transport.peer_mtu("unknown-peer"), BLE_MAX_FRAGMENT_SIZE);
1661 }
1662
1663 #[test]
1664 fn test_ble_set_peer_mtu_records_and_clamps() {
1665 let transport = BleTransport::new("test-device");
1666
1667 transport.set_peer_mtu("alice", 244);
1669 assert_eq!(transport.peer_mtu("alice"), 244);
1670
1671 transport.set_peer_mtu("bob", 20);
1675 assert_eq!(transport.peer_mtu("bob"), BLE_MAX_FRAGMENT_SIZE);
1676 assert!(
1677 !transport.peer_mtus.lock().unwrap().contains_key("bob"),
1678 "undersized MTU report must not be stored"
1679 );
1680
1681 transport.set_peer_mtu("carol", 9999);
1683 assert_eq!(transport.peer_mtu("carol"), MAX_REASONABLE_BLE_PAYLOAD);
1684
1685 transport.set_peer_mtu("dave", BLE_MAX_FRAGMENT_SIZE);
1687 assert_eq!(transport.peer_mtu("dave"), BLE_MAX_FRAGMENT_SIZE);
1688 assert!(transport.peer_mtus.lock().unwrap().contains_key("dave"));
1689
1690 assert_eq!(transport.peer_mtu("alice"), 244);
1692 }
1693
1694 #[test]
1695 fn test_ble_undersized_renegotiation_drops_stored_entry() {
1696 let transport = BleTransport::new("test-device");
1701 transport.set_peer_mtu("alice", 400);
1702 assert_eq!(transport.peer_mtu("alice"), 400);
1703
1704 transport.set_peer_mtu("alice", 20);
1705 assert_eq!(transport.peer_mtu("alice"), BLE_MAX_FRAGMENT_SIZE);
1706 assert!(
1707 !transport.peer_mtus.lock().unwrap().contains_key("alice"),
1708 "undersized renegotiation must drop the stale entry"
1709 );
1710 }
1711
1712 #[test]
1713 fn test_ble_stop_drains_peer_mtus() {
1714 let transport = BleTransport::new("test-device");
1717 transport.set_peer_mtu("alice", 400);
1718 transport.set_peer_mtu("bob", 300);
1719 assert_eq!(transport.peer_mtu("alice"), 400);
1720 assert_eq!(transport.peer_mtu("bob"), 300);
1721
1722 transport.stop().unwrap();
1723
1724 assert!(transport.peer_mtus.lock().unwrap().is_empty());
1725 assert_eq!(transport.peer_mtu("alice"), BLE_MAX_FRAGMENT_SIZE);
1726 assert_eq!(transport.peer_mtu("bob"), BLE_MAX_FRAGMENT_SIZE);
1727 }
1728
1729 #[test]
1730 fn test_ble_stop_drains_all_per_session_state() {
1731 let transport = BleTransport::new("test-device");
1737 transport.start().unwrap();
1738 transport.on_peer_discovered(peer_device("bob"));
1739 transport.set_peer_mtu("bob", 400);
1740 transport.send(&small_message()).unwrap();
1741 let big = Message::builder(
1744 UserId::new("alice").unwrap(),
1745 UserId::new("bob").unwrap(),
1746 AppId::new("app").unwrap(),
1747 )
1748 .content("z".repeat(600))
1749 .build();
1750 let frags = transport.fragment_message(&big).unwrap();
1751 assert!(frags.len() > 1);
1752 transport.on_fragment_received(frags[0].clone()).unwrap();
1753 transport.on_message_received(small_message());
1755
1756 assert!(!transport.peers.lock().unwrap().is_empty());
1757 assert!(!transport.peer_mtus.lock().unwrap().is_empty());
1758 assert!(!transport.fragment_buffers.lock().unwrap().is_empty());
1759 assert!(
1760 !transport.send_queue.lock().unwrap().is_empty()
1761 || !transport.pending_fragments.lock().unwrap().is_empty()
1762 );
1763 assert!(!transport.receive_queue.lock().unwrap().is_empty());
1764
1765 transport.stop().unwrap();
1766
1767 assert!(transport.peers.lock().unwrap().is_empty());
1768 assert!(transport.peer_mtus.lock().unwrap().is_empty());
1769 assert!(transport.fragment_buffers.lock().unwrap().is_empty());
1770 assert!(transport.send_queue.lock().unwrap().is_empty());
1771 assert!(transport.pending_fragments.lock().unwrap().is_empty());
1772 assert!(transport.receive_queue.lock().unwrap().is_empty());
1773 }
1774
1775 #[test]
1776 fn test_ble_on_peer_lost_clears_stored_mtu() {
1777 let transport = BleTransport::new("test-device");
1778 transport.on_peer_discovered(peer_device("alice"));
1779 transport.set_peer_mtu("alice", 400);
1780 assert_eq!(transport.peer_mtu("alice"), 400);
1781
1782 transport.on_peer_lost("alice");
1783 assert!(transport.get_peer("alice").is_none());
1786 assert_eq!(transport.peer_mtu("alice"), BLE_MAX_FRAGMENT_SIZE);
1787 }
1788
1789 #[test]
1790 fn test_ble_clear_peer_mtu_reverts_to_fallback() {
1791 let transport = BleTransport::new("test-device");
1792 transport.set_peer_mtu("alice", 400);
1793 assert_eq!(transport.peer_mtu("alice"), 400);
1794 transport.clear_peer_mtu("alice");
1795 assert_eq!(transport.peer_mtu("alice"), BLE_MAX_FRAGMENT_SIZE);
1796 }
1797
1798 #[test]
1799 fn test_ble_fragment_size_differs_per_peer() {
1800 let transport = BleTransport::new("test-device");
1801 let sender = UserId::new("alice").unwrap();
1802 let app_id = AppId::new("app").unwrap();
1803 let content = "y".repeat(1024);
1804
1805 let msg_small = Message::builder(
1808 sender.clone(),
1809 UserId::new("small-peer").unwrap(),
1810 app_id.clone(),
1811 )
1812 .content(content.clone())
1813 .build();
1814 let msg_big = Message::builder(sender, UserId::new("big-peer").unwrap(), app_id)
1815 .content(content)
1816 .build();
1817
1818 transport.on_peer_discovered(peer_device("small-peer"));
1822 transport.on_peer_discovered(peer_device("big-peer"));
1823 transport.set_peer_mtu("small-peer", BLE_MAX_FRAGMENT_SIZE);
1824 transport.set_peer_mtu("big-peer", 500);
1825
1826 let small_frags = transport.fragment_message(&msg_small).unwrap();
1827 let big_frags = transport.fragment_message(&msg_big).unwrap();
1828
1829 assert!(big_frags.len() < small_frags.len());
1831 let max_small = small_frags.iter().map(|f| f.len()).max().unwrap();
1832 let max_big = big_frags.iter().map(|f| f.len()).max().unwrap();
1833 assert!(max_big > max_small);
1834 assert!(max_small <= BLE_MAX_FRAGMENT_SIZE);
1835 assert!(max_big <= 500);
1836 }
1837
1838 #[test]
1839 fn test_ble_fragment_unknown_peer_uses_fallback() {
1840 let transport = BleTransport::new("test-device");
1841 transport.on_peer_discovered(peer_device("bob"));
1845 let msg = small_message();
1846 let fragments = transport.fragment_message(&msg).unwrap();
1847 for fragment in &fragments {
1848 assert!(fragment.len() <= BLE_MAX_FRAGMENT_SIZE);
1849 }
1850 }
1851
1852 #[test]
1853 fn test_ble_upward_renegotiation_overwrites_stored_mtu() {
1854 let transport = BleTransport::new("test-device");
1860 transport.on_peer_discovered(peer_device("alice"));
1861
1862 transport.set_peer_mtu("alice", 400);
1863 assert_eq!(transport.peer_mtu("alice"), 400);
1864
1865 transport.set_peer_mtu("alice", 500);
1866 assert_eq!(transport.peer_mtu("alice"), 500);
1867
1868 let msg = Message::builder(
1872 UserId::new("sender").unwrap(),
1873 UserId::new("alice").unwrap(),
1874 AppId::new("app").unwrap(),
1875 )
1876 .content("x".repeat(1024))
1877 .build();
1878
1879 transport.set_peer_mtu("alice", 300);
1880 let small_frags = transport.fragment_message(&msg).unwrap();
1881 transport.set_peer_mtu("alice", 500);
1882 let big_frags = transport.fragment_message(&msg).unwrap();
1883
1884 let max_small = small_frags.iter().map(|f| f.len()).max().unwrap();
1885 let max_big = big_frags.iter().map(|f| f.len()).max().unwrap();
1886 assert!(
1887 max_big > max_small,
1888 "upward reneg must produce larger fragments: {} vs {}",
1889 max_big,
1890 max_small
1891 );
1892 assert!(big_frags.len() < small_frags.len());
1893 }
1894
1895 #[test]
1896 fn test_ble_stop_start_round_trip_is_fresh() {
1897 let transport = BleTransport::new("test-device");
1907
1908 transport.start().unwrap();
1909 transport.on_peer_discovered(peer_device("bob"));
1910 transport.set_peer_mtu("bob", 400);
1911 transport.send(&small_message()).unwrap();
1912 assert_eq!(transport.peer_mtu("bob"), 400);
1913
1914 transport.stop().unwrap();
1915 transport.start().unwrap();
1916
1917 assert!(transport.get_peer("bob").is_none());
1920 assert_eq!(transport.peer_mtu("bob"), BLE_MAX_FRAGMENT_SIZE);
1921
1922 let result = transport.send(&small_message());
1927 assert!(
1928 matches!(result, Err(crate::Error::PeerNotReachable(_))),
1929 "send after stop/start must report no reachable peer, got {:?}",
1930 result
1931 );
1932
1933 transport.on_peer_discovered(peer_device("bob"));
1936 transport.set_peer_mtu("bob", 250);
1937 assert_eq!(transport.peer_mtu("bob"), 250);
1938 }
1939
1940 #[test]
1941 fn test_ble_undersized_mtu_reports_counter() {
1942 let transport = BleTransport::new("test-device");
1950 assert_eq!(transport.undersized_mtu_reports(), 0);
1951
1952 transport.set_peer_mtu("alice", 20);
1953 assert_eq!(transport.undersized_mtu_reports(), 1);
1954
1955 transport.set_peer_mtu("bob", 400);
1957 assert_eq!(transport.undersized_mtu_reports(), 1);
1958
1959 transport.set_peer_mtu("carol", BLE_MAX_FRAGMENT_SIZE);
1961 assert_eq!(transport.undersized_mtu_reports(), 1);
1962
1963 transport.set_peer_mtu("dave", 9999);
1965 assert_eq!(transport.undersized_mtu_reports(), 1);
1966
1967 transport.set_peer_mtu("eve", 10);
1969 assert_eq!(transport.undersized_mtu_reports(), 2);
1970
1971 transport.clear_peer_mtu("bob");
1974 assert_eq!(transport.undersized_mtu_reports(), 2);
1975
1976 transport.set_peer_mtu("alice", 15);
1978 assert_eq!(transport.undersized_mtu_reports(), 3);
1979 }
1980
1981 #[test]
1982 fn test_ble_fragment_fallback_count_increments_on_registered_peer_without_mtu() {
1983 let transport = BleTransport::new("test-device");
1994 transport.on_peer_discovered(peer_device("bob"));
1995 assert_eq!(transport.fragment_fallback_count(), 0);
1996
1997 let msg = small_message();
2000 transport.fragment_message(&msg).unwrap();
2001 assert_eq!(transport.fragment_fallback_count(), 1);
2002
2003 transport.fragment_message(&msg).unwrap();
2005 assert_eq!(transport.fragment_fallback_count(), 2);
2006
2007 transport.set_peer_mtu("bob", 400);
2009 transport.fragment_message(&msg).unwrap();
2010 transport.fragment_message(&msg).unwrap();
2011 assert_eq!(transport.fragment_fallback_count(), 2);
2012
2013 transport.clear_peer_mtu("bob");
2016 transport.fragment_message(&msg).unwrap();
2017 assert_eq!(transport.fragment_fallback_count(), 3);
2018 }
2019
2020 #[test]
2021 fn test_ble_fragment_fallback_count_ignores_teardown_race() {
2022 let transport = BleTransport::new("test-device");
2033 transport.on_peer_discovered(peer_device("bob"));
2034 transport.set_peer_mtu("bob", 400);
2035 assert_eq!(transport.fragment_fallback_count(), 0);
2036
2037 transport.on_peer_lost("bob");
2043 let msg = small_message();
2044 transport.fragment_message(&msg).unwrap();
2045
2046 assert_eq!(
2047 transport.fragment_fallback_count(),
2048 0,
2049 "teardown race must not tick the contract-break counter"
2050 );
2051 }
2052
2053 #[test]
2054 fn test_ble_golden_path_handshake_never_falls_back() {
2055 let transport = BleTransport::new("test-device");
2076
2077 transport.set_peer_mtu("bob", 400);
2080 transport.on_peer_discovered(peer_device("bob"));
2081
2082 let msg = small_message();
2083 let fragments = transport.fragment_message(&msg).unwrap();
2084 assert!(!fragments.is_empty());
2085
2086 assert_eq!(
2087 transport.fragment_fallback_count(),
2088 0,
2089 "golden-path handshake must not fall back — the \
2090 platform-side MTU-before-discover invariant regressed, \
2091 or the recipient -> device_id keying contract broke"
2092 );
2093 }
2094
2095 #[test]
2096 fn test_ble_peripheral_role_disconnect_must_not_clear_central_mtu() {
2097 let transport = BleTransport::new("test-device");
2122 transport.on_peer_discovered(peer_device("bob"));
2123 transport.set_peer_mtu("bob", 400);
2124
2125 let msg = Message::builder(
2126 UserId::new("alice").unwrap(),
2127 UserId::new("bob").unwrap(),
2128 AppId::new("app").unwrap(),
2129 )
2130 .content("x".repeat(1024))
2131 .build();
2132
2133 let pre_frags = transport.fragment_message(&msg).unwrap();
2134 let pre_max = pre_frags.iter().map(|f| f.len()).max().unwrap();
2135 assert!(
2136 pre_max > BLE_MAX_FRAGMENT_SIZE,
2137 "pre-disconnect fragments must size against the negotiated 400-byte MTU"
2138 );
2139
2140 let post_frags = transport.fragment_message(&msg).unwrap();
2147 let post_max = post_frags.iter().map(|f| f.len()).max().unwrap();
2148 assert_eq!(
2149 pre_max, post_max,
2150 "peripheral-role disconnect must not change fragment sizing for an alive central-role link"
2151 );
2152 assert_eq!(pre_frags.len(), post_frags.len());
2153 assert_eq!(
2154 transport.fragment_fallback_count(),
2155 0,
2156 "a no-op peripheral-role disconnect must not tick the keying-contract counter"
2157 );
2158 assert_eq!(
2159 transport.peer_mtu("bob"),
2160 400,
2161 "peer MTU must survive a peripheral-role disconnect at the Rust layer"
2162 );
2163 }
2164
2165 #[test]
2166 fn test_ble_platform_handle() {
2167 let transport = BleTransport::new("test-device");
2168 assert_eq!(transport.platform_handle(), None);
2169 transport.set_platform_handle(42);
2170 assert_eq!(transport.platform_handle(), Some(42));
2171 }
2172
2173 #[test]
2174 fn test_ble_update_metrics() {
2175 let transport = BleTransport::new("test-device");
2176 let mut m = TransportMetrics::default();
2177 m.rssi = Some(-70);
2178 transport.update_metrics(m);
2179 assert_eq!(transport.metrics().rssi, Some(-70));
2180 }
2181
2182 #[test]
2183 fn test_ble_peer_discovery() {
2184 let transport = BleTransport::new("test-device");
2185 transport.on_peer_discovered(peer_device("peer-1"));
2186 let peers = transport.get_peers();
2187 assert_eq!(peers.len(), 1);
2188 assert_eq!(peers[0].device_id, "peer-1");
2189 assert!(transport.get_peer("peer-1").is_some());
2190 assert!(transport.get_peer("other").is_none());
2191 }
2192
2193 #[test]
2194 fn test_ble_peer_lost() {
2195 let transport = BleTransport::new("test-device");
2196 transport.on_peer_discovered(peer_device("peer-1"));
2197 transport.on_peer_lost("peer-1");
2198 assert_eq!(transport.get_peers().len(), 0);
2199 }
2200
2201 #[test]
2202 fn test_ble_has_pending_sends_dequeue_send_get_queue_depth() {
2203 let transport = BleTransport::new("test-device");
2204 transport.start().unwrap();
2205 transport.on_peer_discovered(peer_device("bob"));
2206 let msg = small_message();
2207 transport.send(&msg).unwrap();
2208 assert!(transport.has_pending_sends());
2209 assert_eq!(transport.get_queue_depth(), 1);
2210 let dequeued = transport.dequeue_send();
2211 assert!(dequeued.is_some());
2212 assert!(!transport.has_pending_sends());
2213 assert_eq!(transport.get_queue_depth(), 0);
2214 assert!(transport.dequeue_send().is_none());
2215 }
2216
2217 #[test]
2218 fn test_ble_serialize_deserialize_message() {
2219 let transport = BleTransport::new("test-device");
2220 let msg = small_message();
2221 let data = transport.serialize_message(&msg).unwrap();
2222 let back = transport.deserialize_message(&data).unwrap();
2223 assert_eq!(back.id, msg.id);
2224 assert_eq!(back.content, msg.content);
2225 }
2226
2227 #[test]
2228 fn test_ble_deserialize_invalid_json() {
2229 let transport = BleTransport::new("test-device");
2230 let result = transport.deserialize_message(b"not json");
2231 assert!(result.is_err());
2232 assert!(matches!(
2233 result.unwrap_err(),
2234 crate::Error::SerializationError(_)
2235 ));
2236 }
2237
2238 #[test]
2239 fn test_ble_single_fragment_roundtrip() {
2240 let transport = BleTransport::new("test-device");
2241 transport.on_peer_discovered(peer_device("bob"));
2242 transport.set_peer_mtu("bob", 512);
2243 let msg = small_message();
2244 let fragments = transport.fragment_message(&msg).unwrap();
2245 assert_eq!(
2246 fragments.len(),
2247 1,
2248 "small message with large MTU should fit in one fragment"
2249 );
2250 let reconstructed = transport.process_fragment(&fragments[0]).unwrap();
2251 assert!(reconstructed.is_some());
2252 assert_eq!(reconstructed.unwrap().content, msg.content);
2253 }
2254
2255 #[test]
2256 fn test_ble_fragment_roundtrip() {
2257 let transport = BleTransport::new("test-device");
2258 transport.on_peer_discovered(peer_device("bob"));
2259 let sender = UserId::new("alice").unwrap();
2260 let recipient = UserId::new("bob").unwrap();
2261 let app_id = AppId::new("app").unwrap();
2262 let content = "x".repeat(512);
2263
2264 let message = Message::builder(sender, recipient, app_id)
2265 .content(content.clone())
2266 .priority(MessagePriority::High)
2267 .ttl(TTL::new(8).unwrap())
2268 .build();
2269
2270 let fragments = transport.fragment_message(&message).unwrap();
2271 assert!(fragments.len() > 1);
2272 for fragment in &fragments {
2273 assert!(fragment.len() <= BLE_MAX_FRAGMENT_SIZE);
2274 }
2275
2276 let mut reconstructed = None;
2277 for fragment in fragments {
2278 if let Some(msg) = transport.process_fragment(&fragment).unwrap() {
2279 reconstructed = Some(msg);
2280 }
2281 }
2282
2283 let reconstructed = reconstructed.expect("Expected complete message");
2284 assert_eq!(reconstructed.content, content);
2285 }
2286
2287 #[test]
2288 fn test_ble_fragment_roundtrip_binary_wire() {
2289 let transport = BleTransport::new("test-device");
2293 transport.on_peer_discovered(peer_device("bob"));
2294 let sender = UserId::new("alice").unwrap();
2295 let recipient = UserId::new("bob").unwrap();
2296 let app_id = AppId::new("app").unwrap();
2297 let content = "x".repeat(512);
2298
2299 let mut message = Message::builder(sender, recipient, app_id)
2300 .content(content.clone())
2301 .priority(MessagePriority::High)
2302 .ttl(TTL::new(8).unwrap())
2303 .build();
2304 message.set_wire_codec(offline_protocol_core::WireCodec::BinaryV1);
2308 assert_eq!(
2309 transport.serialize_message(&message).unwrap().first(),
2310 Some(&offline_protocol_core::WIRE_V1_MAGIC),
2311 "a binary-stamped message serializes to a wire-v1 frame"
2312 );
2313
2314 let fragments = transport.fragment_message(&message).unwrap();
2315 assert!(
2316 fragments.len() > 1,
2317 "512-byte content must span multiple BLE fragments"
2318 );
2319
2320 let mut reconstructed = None;
2321 for fragment in fragments {
2322 if let Some(msg) = transport.process_fragment(&fragment).unwrap() {
2323 reconstructed = Some(msg);
2324 }
2325 }
2326 let reconstructed = reconstructed.expect("Expected complete message");
2327 assert_eq!(reconstructed.content, content);
2328 assert_eq!(reconstructed.id, message.id);
2329 assert_eq!(
2332 reconstructed.wire_codec(),
2333 offline_protocol_core::WireCodec::Json
2334 );
2335 }
2336
2337 #[test]
2338 fn test_ble_process_fragment_invalid_magic() {
2339 let transport = BleTransport::new("test-device");
2340 let mut bad = vec![0x00, 0x00, 1, 0, 0, 0, 0, 0, 0, 0]; bad.extend_from_slice(b"{}");
2342 let result = transport.process_fragment(&bad);
2343 assert!(result.is_err());
2344 assert!(matches!(result.unwrap_err(), crate::Error::Other(s) if s.contains("magic")));
2345 }
2346
2347 #[test]
2348 fn test_ble_process_fragment_too_short() {
2349 let transport = BleTransport::new("test-device");
2350 let result = transport.process_fragment(&[0x4f, 0x50]); assert!(result.is_err());
2352 assert!(
2353 matches!(result.unwrap_err(), crate::Error::Other(s) if s.contains("short") || s.contains("truncat"))
2354 );
2355 }
2356
2357 #[test]
2358 fn test_ble_process_fragment_wrong_version() {
2359 let transport = BleTransport::new("test-device");
2360 let bad = [b'O', b'P', 99u8, 0u8, 0u8, 0u8, 1u8, 0u8, 0u8, 0u8];
2362 let result = transport.process_fragment(&bad);
2363 assert!(result.is_err());
2364 assert!(matches!(result.unwrap_err(), crate::Error::Other(s) if s.contains("version")));
2365 }
2366
2367 #[test]
2368 fn test_ble_on_fragment_received_complete_queues_message() {
2369 let transport = BleTransport::new("test-device");
2370 transport.on_peer_discovered(peer_device("bob"));
2371 transport.set_peer_mtu("bob", 512);
2372 let msg = small_message();
2373 let fragments = transport.fragment_message(&msg).unwrap();
2374 for fragment in &fragments {
2375 transport.on_fragment_received(fragment.clone()).unwrap();
2376 }
2377 let received = transport.receive().unwrap();
2378 assert!(received.is_some());
2379 assert_eq!(received.unwrap().content, msg.content);
2380 }
2381
2382 #[test]
2383 fn test_ble_on_fragment_received_bad_data_drops_ok() {
2384 let transport = BleTransport::new("test-device");
2385 let result = transport.on_fragment_received(vec![0u8; 5]);
2386 assert!(result.is_ok()); }
2388
2389 #[test]
2390 fn test_ble_get_next_fragment_requeue() {
2391 let transport = BleTransport::new("test-device");
2392 transport.start().unwrap();
2393 transport.on_peer_discovered(peer_device("bob"));
2394 let msg = small_message();
2395 transport.send(&msg).unwrap();
2396 let first = transport.get_next_fragment().unwrap();
2397 assert!(first.is_some());
2398 let (recipient, data) = first.unwrap();
2399 transport.requeue_fragment(&recipient, data.clone());
2400 let again = transport.get_next_fragment().unwrap();
2401 assert!(again.is_some());
2402 assert_eq!(again.unwrap().1, data);
2403 }
2404
2405 #[test]
2406 fn test_ble_get_next_fragment_none_when_empty() {
2407 let transport = BleTransport::new("test-device");
2408 assert!(transport.get_next_fragment().unwrap().is_none());
2409 }
2410
2411 #[test]
2412 fn test_ble_on_message_received() {
2413 let transport = BleTransport::new("test-device");
2414 let msg = small_message();
2415 transport.on_message_received(msg.clone());
2416 let received = transport.receive().unwrap();
2417 assert!(received.is_some());
2418 assert_eq!(received.unwrap().id, msg.id);
2419 }
2420
2421 #[test]
2422 fn test_ble_send_rejects_unknown_peer() {
2423 let transport = BleTransport::new("test-device");
2424 transport.start().unwrap();
2425 let msg = small_message();
2426 let result = transport.send(&msg);
2427 assert!(result.is_err());
2428 assert!(matches!(
2429 result.unwrap_err(),
2430 crate::Error::PeerNotReachable(_)
2431 ));
2432 }
2433
2434 #[test]
2435 fn test_ble_send_allows_known_peer() {
2436 let transport = BleTransport::new("test-device");
2437 transport.start().unwrap();
2438 transport.on_peer_discovered(peer_device("bob"));
2439 let msg = small_message();
2440 assert!(transport.send(&msg).is_ok());
2441 }
2442
2443 #[test]
2444 fn test_ble_send_after_peer_lost() {
2445 let transport = BleTransport::new("test-device");
2446 transport.start().unwrap();
2447 transport.on_peer_discovered(peer_device("bob"));
2448 assert!(transport.send(&small_message()).is_ok());
2449 transport.on_peer_lost("bob");
2450 let result = transport.send(&small_message());
2451 assert!(matches!(
2452 result.unwrap_err(),
2453 crate::Error::PeerNotReachable(_)
2454 ));
2455 }
2456
2457 #[test]
2458 fn test_ble_transport_builder() {
2459 let transport = BleTransportBuilder::new("my-device").build();
2460 assert_eq!(transport.device_id(), "my-device");
2461 assert_eq!(transport.transport_type(), TransportType::BLE);
2462 }
2463
2464 #[test]
2465 fn test_ble_reassembled_payload_rejects_oversized() {
2466 let transport = BleTransport::new("test-device");
2467 let chunk_size: usize = 60_000;
2475 let num_fragments = (DEFAULT_MAX_MESSAGE_SIZE / chunk_size) + 2;
2476 let overflow_at = DEFAULT_MAX_MESSAGE_SIZE / chunk_size;
2479
2480 for i in 0..num_fragments {
2481 let frag = encode_fragment(
2482 b"big",
2483 i as u16,
2484 num_fragments as u16,
2485 &vec![0xAA; chunk_size],
2486 )
2487 .unwrap();
2488
2489 let result = transport.process_fragment(&frag);
2490 if i < overflow_at {
2491 assert!(result.unwrap().is_none());
2493 } else {
2494 assert!(matches!(
2497 result.unwrap_err(),
2498 crate::Error::MessageTooLarge(_, _)
2499 ));
2500 break;
2501 }
2502 }
2503
2504 assert!(transport.fragment_buffers.lock().unwrap().is_empty());
2506 }
2507
2508 #[test]
2509 fn test_ble_process_fragment_rejects_excessive_total() {
2510 let transport = BleTransport::new("test-device");
2511
2512 let over = encode_fragment(b"evil", 0, (BLE_MAX_FRAGMENT_COUNT + 1) as u16, b"x").unwrap();
2515 assert!(matches!(
2516 transport.process_fragment(&over).unwrap_err(),
2517 crate::Error::Other(s) if s.contains("Fragment count exceeds maximum")
2518 ));
2519
2520 let max = encode_fragment(b"evil", 0, u16::MAX, b"x").unwrap();
2522 assert!(transport.process_fragment(&max).is_err());
2523
2524 assert!(transport.fragment_buffers.lock().unwrap().is_empty());
2526
2527 let ok = encode_fragment(b"good", 0, BLE_MAX_FRAGMENT_COUNT as u16, b"x").unwrap();
2530 assert!(transport.process_fragment(&ok).unwrap().is_none());
2531 assert_eq!(transport.fragment_buffers.lock().unwrap().len(), 1);
2532 }
2533
2534 #[test]
2535 fn test_ble_incremental_size_bound_rejects_before_completion() {
2536 let transport = BleTransport::new("test-device");
2537
2538 let total_fragments: u16 = 30;
2542 let chunk_size: usize = 60_000;
2543 let overflow_at = DEFAULT_MAX_MESSAGE_SIZE / chunk_size;
2544
2545 let mut rejected_at = None;
2546 for i in 0..total_fragments as usize {
2547 let frag = encode_fragment(
2548 b"partial",
2549 i as u16,
2550 total_fragments,
2551 &vec![0x5A; chunk_size],
2552 )
2553 .unwrap();
2554 match transport.process_fragment(&frag) {
2555 Ok(opt) => assert!(opt.is_none(), "fragment {i} should not complete"),
2556 Err(crate::Error::MessageTooLarge(_, _)) => {
2557 rejected_at = Some(i);
2558 break;
2559 }
2560 Err(e) => panic!("unexpected error at fragment {i}: {e}"),
2561 }
2562 }
2563
2564 let rejected_at = rejected_at.expect("size bound should have rejected before completion");
2565 assert_eq!(
2566 rejected_at, overflow_at,
2567 "rejection should fire on the first overflowing fragment"
2568 );
2569 assert!(
2570 rejected_at < total_fragments as usize - 1,
2571 "rejection must happen strictly before the assembly could complete"
2572 );
2573 assert!(transport.fragment_buffers.lock().unwrap().is_empty());
2575 }
2576
2577 #[test]
2578 fn test_ble_duplicate_fragment_index_does_not_inflate_size() {
2579 let transport = BleTransport::new("test-device");
2580
2581 let chunk_size: usize = 60_000;
2585 for _ in 0..100 {
2586 let frag = encode_fragment(b"dup", 0, 4, &vec![0xC3; chunk_size]).unwrap();
2587 assert!(
2588 transport.process_fragment(&frag).unwrap().is_none(),
2589 "duplicate index should never trip the size bound"
2590 );
2591 }
2592
2593 transport.on_peer_discovered(peer_device("bob"));
2595 transport.set_peer_mtu("bob", 244);
2596 let msg = small_message();
2597 let fragments = transport.fragment_message(&msg).unwrap();
2598 let mut done = None;
2599 for f in fragments {
2600 if let Some(m) = transport.process_fragment(&f).unwrap() {
2601 done = Some(m);
2602 }
2603 }
2604 assert_eq!(
2605 done.expect("message should reassemble").content,
2606 msg.content
2607 );
2608 }
2609
2610 #[test]
2611 fn test_ble_process_fragment_rejects_out_of_range_index() {
2612 let transport = BleTransport::new("test-device");
2613
2614 let total: u16 = 4;
2619 for bad_index in [total, total + 1, 100, u16::MAX] {
2620 let frag = encode_fragment(b"oob", bad_index, total, b"x").unwrap();
2621 assert!(
2622 matches!(
2623 transport.process_fragment(&frag).unwrap_err(),
2624 crate::Error::Other(s) if s.contains("Fragment index out of range")
2625 ),
2626 "index {bad_index} (total {total}) should be rejected"
2627 );
2628 }
2629
2630 assert!(transport.fragment_buffers.lock().unwrap().is_empty());
2632
2633 let ok = encode_fragment(b"oob", total - 1, total, b"x").unwrap();
2635 assert!(transport.process_fragment(&ok).unwrap().is_none());
2636 assert_eq!(transport.fragment_buffers.lock().unwrap().len(), 1);
2637 }
2638
2639 #[test]
2640 fn test_ble_process_fragment_rejects_zero_total() {
2641 let transport = BleTransport::new("test-device");
2642
2643 for index in [0u16, 1, u16::MAX] {
2648 let frag = encode_fragment(b"zero", index, 0, b"x").unwrap();
2649 assert!(
2650 matches!(
2651 transport.process_fragment(&frag).unwrap_err(),
2652 crate::Error::Other(s) if s.contains("Fragment index out of range")
2653 ),
2654 "total_fragments == 0 (index {index}) should be rejected"
2655 );
2656 }
2657 assert!(transport.fragment_buffers.lock().unwrap().is_empty());
2658 }
2659
2660 #[test]
2661 fn test_ble_distinct_out_of_range_indices_cannot_bloat_assembly_map() {
2662 let transport = BleTransport::new("test-device");
2663
2664 let total: u16 = 2;
2672 for index in 2u16..5_000 {
2673 let frag = encode_fragment(b"flood", index, total, b"").unwrap();
2674 assert!(
2675 transport.process_fragment(&frag).is_err(),
2676 "out-of-range index {index} must be rejected, not buffered"
2677 );
2678 }
2679 assert!(
2680 transport.fragment_buffers.lock().unwrap().is_empty(),
2681 "out-of-range indices must never create an assembly"
2682 );
2683 }
2684
2685 #[test]
2686 fn test_fragment_eviction_fires_callback() {
2687 let transport = BleTransport::new("test-device");
2688
2689 let evictions: Arc<Mutex<Vec<FragmentEvictionInfo>>> = Arc::new(Mutex::new(Vec::new()));
2690 let evictions_clone = evictions.clone();
2691 transport.set_fragment_eviction_callback(Some(Arc::new(move |info| {
2692 evictions_clone.lock().unwrap().push(info);
2693 })));
2694
2695 for i in 0..BLE_MAX_FRAGMENT_ASSEMBLIES {
2698 let msg_id = format!("msg-{i}");
2699 let frag = encode_fragment(msg_id.as_bytes(), 0, 4, b"payload-data").unwrap();
2700 let result = transport.process_fragment(&frag).unwrap();
2701 assert!(result.is_none(), "partial fragment should not complete");
2702 }
2703
2704 assert!(evictions.lock().unwrap().is_empty());
2706
2707 let trigger_frag = encode_fragment(b"new-msg", 0, 4, b"payload-data").unwrap();
2709 let result = transport.process_fragment(&trigger_frag).unwrap();
2710 assert!(result.is_none());
2711
2712 let captured = evictions.lock().unwrap();
2713 assert_eq!(captured.len(), 1, "exactly one eviction should fire");
2714 assert_eq!(captured[0].completion_percent, 25);
2715 assert!(
2716 captured[0].message_id.starts_with("msg-"),
2717 "evicted entry should be one of the pre-filled assemblies"
2718 );
2719 }
2720
2721 #[test]
2722 fn test_fragment_eviction_callback_can_reenter_transport() {
2723 let transport = Arc::new(BleTransport::new("test-device"));
2724
2725 let fired = Arc::new(Mutex::new(false));
2726 let fired_clone = Arc::clone(&fired);
2727 let transport_clone = Arc::clone(&transport);
2728 transport.set_fragment_eviction_callback(Some(Arc::new(move |_info| {
2729 transport_clone.set_fragment_eviction_callback(None);
2734 *fired_clone.lock().unwrap() = true;
2735 })));
2736
2737 for i in 0..BLE_MAX_FRAGMENT_ASSEMBLIES {
2741 let msg_id = format!("msg-{i}");
2742 let frag = encode_fragment(msg_id.as_bytes(), 0, 4, b"payload-data").unwrap();
2743 assert!(transport.process_fragment(&frag).unwrap().is_none());
2744 }
2745 let trigger_frag = encode_fragment(b"new-msg", 0, 4, b"payload-data").unwrap();
2746 assert!(transport.process_fragment(&trigger_frag).unwrap().is_none());
2747
2748 assert!(
2749 *fired.lock().unwrap(),
2750 "eviction callback should have fired"
2751 );
2752 }
2753
2754 #[test]
2759 fn a_capacity_eviction_is_not_a_send_failure() {
2760 let transport = BleTransport::new("test-device");
2761 for i in 0..BLE_MAX_FRAGMENT_ASSEMBLIES {
2762 let msg_id = format!("msg-{i}");
2763 let frag = encode_fragment(msg_id.as_bytes(), 0, 4, b"payload-data").unwrap();
2764 assert!(transport.process_fragment(&frag).unwrap().is_none());
2765 }
2766 let trigger_frag = encode_fragment(b"new-msg", 0, 4, b"payload-data").unwrap();
2767 assert!(transport.process_fragment(&trigger_frag).unwrap().is_none());
2768 assert_eq!(
2769 transport.fragment_buffers.lock_or_recover().len(),
2770 BLE_MAX_FRAGMENT_ASSEMBLIES,
2771 "one assembly must have been evicted to admit the new one"
2772 );
2773
2774 let metrics = transport.metrics();
2775 assert_eq!(metrics.sample_count(), 0);
2776 assert_eq!(metrics.delivery_ratio, None);
2777 assert_eq!(metrics.drop_rate, None);
2778 }
2779
2780 #[test]
2783 fn an_idle_expired_assembly_is_not_a_send_failure() {
2784 let transport = BleTransport::new("test-device");
2785 let stale = SystemTime::now() - StdDuration::from_secs(BLE_FRAGMENT_TIMEOUT_SECS + 1);
2786 transport.fragment_buffers.lock_or_recover().insert(
2787 "stale-msg".to_string(),
2788 FragmentAssembly {
2789 total_fragments: 4,
2790 fragments: HashMap::from([(0, b"payload-data".to_vec())]),
2791 started_at: stale,
2792 last_seen: stale,
2793 buffered_bytes: 12,
2794 },
2795 );
2796
2797 let frag = encode_fragment(b"fresh-msg", 0, 4, b"payload-data").unwrap();
2799 assert!(transport.process_fragment(&frag).unwrap().is_none());
2800
2801 let buffers = transport.fragment_buffers.lock_or_recover();
2802 assert!(!buffers.contains_key("stale-msg"), "stale assembly expired");
2803 assert!(buffers.contains_key("fresh-msg"));
2804 drop(buffers);
2805
2806 let metrics = transport.metrics();
2807 assert_eq!(metrics.sample_count(), 0);
2808 assert_eq!(metrics.delivery_ratio, None);
2809 assert_eq!(metrics.drop_rate, None);
2810 }
2811
2812 #[test]
2813 fn an_acknowledgement_is_queued_ahead_of_data_but_behind_earlier_acknowledgements() {
2814 let transport = BleTransport::new("test-device");
2815 transport.on_status_changed(TransportStatus::Available);
2816 transport.on_peer_discovered(peer_device("bob"));
2817
2818 let data: Vec<Message> = (0..3).map(|_| small_message()).collect();
2819 let mut ack = small_message();
2820 ack.metadata
2821 .insert(ACK_FOR_KEY.to_string(), "first".to_string());
2822 let mut second_ack = small_message();
2823 second_ack
2824 .metadata
2825 .insert(ACK_FOR_KEY.to_string(), "second".to_string());
2826
2827 transport.send(&data[0]).unwrap();
2828 transport.send(&data[1]).unwrap();
2829 transport.send(&ack).unwrap();
2830 transport.send(&data[2]).unwrap();
2831 transport.send(&second_ack).unwrap();
2832
2833 let order: Vec<_> = transport
2834 .send_queue
2835 .lock()
2836 .unwrap()
2837 .iter()
2838 .map(|(_, message)| message.id.clone())
2839 .collect();
2840 assert_eq!(
2841 order,
2842 vec![
2843 ack.id.clone(),
2844 second_ack.id.clone(),
2845 data[0].id.clone(),
2846 data[1].id.clone(),
2847 data[2].id.clone()
2848 ],
2849 "acknowledgements stop retry timers; queued behind data they arrived too late"
2850 );
2851 }
2852}