1use bmrng::unbounded::{UnboundedRequestReceiver, UnboundedRequestSender};
16use chrono::Utc;
17use livekit_common::{
18 ClientCapability, ParticipantIdentity, RemoteParticipantRegistry,
19 CLIENT_PROTOCOL_DATA_STREAM_V2,
20};
21use livekit_protocol as proto;
22use std::{path::Path, sync::Arc};
23use tokio::sync::Mutex;
24
25use crate::{
26 info::{ByteStreamInfo, TextStreamInfo},
27 types::{ByteHeader, CompressionType, ContentHeader, Header, StreamId, TextHeader},
28 utf8_chunk::Utf8AwareChunkExt,
29 utils::{SendError, StreamError, StreamResult},
30};
31
32use super::{
33 constants,
34 raw_stream::{RawStream, RawStreamOpenOptions},
35 stream_writer::{ByteStreamWriter, TextStreamWriter},
36 StreamByteOptions, StreamTextOptions,
37};
38
39fn create_random_uuid() -> String {
41 uuid::Uuid::new_v4().to_string()
42}
43
44#[derive(Clone)]
45pub struct Manager {
46 packet_tx: UnboundedRequestSender<Vec<proto::DataPacket>, Result<(), SendError>>,
50}
51
52impl Manager {
53 pub fn new() -> (Self, UnboundedRequestReceiver<Vec<proto::DataPacket>, Result<(), SendError>>)
54 {
55 let (packet_tx, packet_rx) = bmrng::unbounded_channel();
56 let manager = Self { packet_tx };
57 (manager, packet_rx)
58 }
59
60 pub async fn stream_text(&self, options: StreamTextOptions) -> StreamResult<TextStreamWriter> {
61 let stream_id: StreamId = options.id.clone().unwrap_or_else(create_random_uuid).into();
63 let dests = options.destination_identities.clone();
64 let (header, text_header) =
65 build_text_header(&options, stream_id, None, None, CompressionType::None);
66 enforce_header_size(&header, &dests)?;
67
68 let open_options = RawStreamOpenOptions {
69 header: header.clone(),
70 destination_identities: dests,
71 sender_identity: options.sender_identity.clone(),
72 packet_tx: self.packet_tx.clone(),
73 };
74 let writer = TextStreamWriter::new(
75 Arc::new(TextStreamInfo::from_headers(header, text_header)),
76 Arc::new(Mutex::new(RawStream::open(open_options).await?)),
77 );
78 Ok(writer)
79 }
80
81 pub async fn stream_bytes(&self, options: StreamByteOptions) -> StreamResult<ByteStreamWriter> {
82 let stream_id: StreamId = options.id.clone().unwrap_or_else(create_random_uuid).into();
83 let name = options.name.clone().unwrap_or_default();
84 let dests = options.destination_identities.clone();
85 let (header, byte_header) = build_byte_header(
86 &options,
87 stream_id,
88 name,
89 options.total_length,
90 None,
91 CompressionType::None,
92 );
93 enforce_header_size(&header, &dests)?;
94
95 let open_options = RawStreamOpenOptions {
96 header: header.clone(),
97 destination_identities: dests,
98 sender_identity: options.sender_identity.clone(),
99 packet_tx: self.packet_tx.clone(),
100 };
101 let writer = ByteStreamWriter::new(
102 Arc::new(ByteStreamInfo::from_headers(header, byte_header)),
103 Arc::new(Mutex::new(RawStream::open(open_options).await?)),
104 );
105 Ok(writer)
106 }
107
108 pub async fn send_text(
109 &self,
110 text: &str,
111 options: StreamTextOptions,
112 remote_participant_registry: &dyn RemoteParticipantRegistry,
113 ) -> StreamResult<TextStreamInfo> {
114 let stream_id: StreamId = options.id.clone().unwrap_or_else(create_random_uuid).into();
115 let total_length = text.len() as u64;
116
117 let eligibility =
118 evaluate_eligibility(remote_participant_registry, &options.destination_identities);
119 let can_compress = options.compress.unwrap_or(true) && eligibility.compression;
120
121 let text_bytes = text.as_bytes();
122 let mut maybe_compressed = MaybeCollectedAsyncReader::from_async_reader(
123 async_compression::futures::bufread::DeflateEncoder::new(
124 futures_util::io::Cursor::new(text_bytes.to_vec()),
125 ),
126 );
127
128 let use_compression = can_compress
131 && maybe_compressed.as_bytes().await.is_ok_and(|c| c.len() < text_bytes.len());
132
133 let (mut header, text_header) = if use_compression {
135 build_text_header(
136 &options,
137 stream_id.clone(),
138 Some(total_length),
139 Some(maybe_compressed.as_bytes().await?.to_owned()),
140 CompressionType::DeflateRaw,
141 )
142 } else {
143 build_text_header(
144 &options,
145 stream_id.clone(),
146 Some(total_length),
147 Some(text_bytes.to_vec()),
148 CompressionType::None,
149 )
150 };
151
152 let proto_header = header.clone().into();
153 if eligibility.inline
154 && options.attached_stream_ids.is_empty()
155 && header_packet_fits(&proto_header, &options.destination_identities)
156 {
157 let mut packet =
158 RawStream::create_header_packet(proto_header, options.destination_identities);
159 packet.participant_identity =
160 options.sender_identity.map(|id| id.into()).unwrap_or_default();
161 RawStream::send_packet(&self.packet_tx, packet).await?;
162 return Ok(TextStreamInfo::from_headers(header, text_header));
163 }
164
165 header.inline_content = None;
168 enforce_header_size(&header, &options.destination_identities)?;
169
170 let info = TextStreamInfo::from_headers(header.clone(), text_header);
171 if use_compression {
172 let compressed_bytes = maybe_compressed.as_bytes().await?;
173 self.send_one_shot_stream(
174 header,
175 compressed_bytes.chunks(constants::STREAM_CHUNK_SIZE_BYTES),
176 options.destination_identities,
177 options.sender_identity,
178 )
179 .await?;
180 } else {
181 self.send_one_shot_stream(
182 header,
183 text_bytes.utf8_aware_chunks(constants::STREAM_CHUNK_SIZE_BYTES),
184 options.destination_identities,
185 options.sender_identity,
186 )
187 .await?;
188 }
189 Ok(info)
190 }
191
192 pub async fn send_bytes(
202 &self,
203 data: impl AsRef<[u8]>,
204 options: StreamByteOptions,
205 remote_participant_registry: &dyn RemoteParticipantRegistry,
206 ) -> StreamResult<ByteStreamInfo> {
207 if options.total_length.is_some() {
208 log::warn!("Ignoring total_length option specified for send_bytes");
209 }
210 let bytes = data.as_ref();
211 let stream_id: StreamId = options.id.clone().unwrap_or_else(create_random_uuid).into();
212 let name = options.name.clone().unwrap_or_else(|| constants::BYTE_DEFAULT_NAME.to_owned());
213 let total_length = bytes.len() as u64;
214
215 let eligibility =
216 evaluate_eligibility(remote_participant_registry, &options.destination_identities);
217 let can_compress = options.compress.unwrap_or(true) && eligibility.compression;
218
219 let mut maybe_compressed = MaybeCollectedAsyncReader::from_async_reader(
220 async_compression::futures::bufread::DeflateEncoder::new(
221 futures_util::io::Cursor::new(bytes.to_vec()),
222 ),
223 );
224
225 let use_compression =
228 can_compress && maybe_compressed.as_bytes().await.is_ok_and(|c| c.len() < bytes.len());
229
230 let (mut header, byte_header) = if use_compression {
232 build_byte_header(
233 &options,
234 stream_id.clone(),
235 name.clone(),
236 Some(total_length), Some(maybe_compressed.as_bytes().await?.to_owned()),
238 CompressionType::DeflateRaw,
239 )
240 } else {
241 build_byte_header(
242 &options,
243 stream_id.clone(),
244 name.clone(),
245 Some(total_length), Some(bytes.to_vec()),
247 CompressionType::None,
248 )
249 };
250
251 let proto_header = header.clone().into();
252 if eligibility.inline && header_packet_fits(&proto_header, &options.destination_identities)
253 {
254 let mut packet =
255 RawStream::create_header_packet(proto_header, options.destination_identities);
256 packet.participant_identity =
257 options.sender_identity.map(|id| id.into()).unwrap_or_default();
258 RawStream::send_packet(&self.packet_tx, packet).await?;
259 return Ok(ByteStreamInfo::from_headers(header, byte_header));
260 }
261
262 header.inline_content = None;
265 enforce_header_size(&header, &options.destination_identities)?;
266
267 let info = ByteStreamInfo::from_headers(header.clone(), byte_header);
268 let content = if use_compression { maybe_compressed.as_bytes().await? } else { bytes };
269 self.send_one_shot_stream(
270 header,
271 content.chunks(constants::STREAM_CHUNK_SIZE_BYTES),
272 options.destination_identities,
273 options.sender_identity,
274 )
275 .await?;
276 Ok(info)
277 }
278
279 pub async fn send_file(
285 &self,
286 path: impl AsRef<Path>,
287 options: StreamByteOptions,
288 remote_participant_registry: &dyn RemoteParticipantRegistry,
289 ) -> StreamResult<ByteStreamInfo> {
290 let path = path.as_ref();
291 let file_size = tokio::fs::metadata(path)
292 .await
293 .map(|metadata| metadata.len())
294 .map_err(StreamError::from)?;
295 let name = options.name.clone().unwrap_or_else(|| {
296 path.file_name().and_then(|n| n.to_str()).unwrap_or_default().to_owned()
297 });
298 let stream_id: StreamId = options.id.clone().unwrap_or_else(create_random_uuid).into();
299 let dests = options.destination_identities.clone();
300
301 let eligibility = evaluate_eligibility(remote_participant_registry, &dests);
302 let should_compress = options.compress.unwrap_or(true) && eligibility.compression;
303 let compression =
304 if should_compress { CompressionType::DeflateRaw } else { CompressionType::None };
305
306 let (header, byte_header) =
307 build_byte_header(&options, stream_id, name, Some(file_size), None, compression);
308 enforce_header_size(&header, &dests)?;
309
310 let open_options = RawStreamOpenOptions {
311 header: header.clone(),
312 destination_identities: dests,
313 sender_identity: options.sender_identity.clone(),
314 packet_tx: self.packet_tx.clone(),
315 };
316 let info = ByteStreamInfo::from_headers(header, byte_header);
317 let mut stream = RawStream::open(open_options).await?;
318 stream.write_file(path, should_compress).await?;
319 stream.close(None, None).await?;
320 Ok(info)
321 }
322
323 async fn send_one_shot_stream<'a>(
329 &self,
330 header: Header,
331 chunks: impl IntoIterator<Item = &'a [u8]>,
332 destination_identities: Vec<ParticipantIdentity>,
333 sender_identity: Option<ParticipantIdentity>,
334 ) -> StreamResult<()> {
335 let stream_id = header.stream_id.to_string();
336 let destinations: Vec<String> =
337 destination_identities.iter().map(|id| id.0.clone()).collect();
338 let mut packets =
339 vec![RawStream::create_header_packet(header.into(), destination_identities)];
340 packets.extend(chunks.into_iter().enumerate().map(|(index, chunk)| {
341 RawStream::create_chunk_packet(&stream_id, index as u64, chunk, &destinations)
342 }));
343 packets.push(RawStream::create_trailer_packet(&stream_id, None, None, &destinations));
344 if let Some(sender_identity) = sender_identity {
345 let identity: String = sender_identity.into();
346 for packet in &mut packets {
347 packet.participant_identity = identity.clone();
348 }
349 }
350 RawStream::send_packets(&self.packet_tx, packets).await
351 }
352}
353
354struct SendEligibility {
356 inline: bool,
358 compression: bool,
360}
361
362fn evaluate_eligibility(
367 registry: &dyn RemoteParticipantRegistry,
368 destinations: &[ParticipantIdentity],
369) -> SendEligibility {
370 let recipients: Vec<ParticipantIdentity> =
371 if destinations.is_empty() { registry.remote_identities() } else { destinations.to_vec() };
372 let inline = recipients
373 .iter()
374 .all(|id| registry.remote_client_protocol(id) >= CLIENT_PROTOCOL_DATA_STREAM_V2);
375 let compression = inline
376 && recipients.iter().all(|id| {
377 registry.remote_capabilities(id).contains(&ClientCapability::CompressionDeflateRaw)
378 });
379
380 SendEligibility { inline, compression }
381}
382
383enum MaybeCollectedAsyncReader<Reader: futures_util::io::AsyncRead + Unpin> {
390 Reader(Reader),
391 Collected(Vec<u8>),
392}
393
394impl<Reader: futures_util::io::AsyncRead + Unpin> MaybeCollectedAsyncReader<Reader> {
395 fn from_async_reader(reader: Reader) -> Self {
396 Self::Reader(reader)
397 }
398
399 async fn as_bytes(&mut self) -> Result<&[u8], std::io::Error> {
400 use futures_util::io::AsyncReadExt;
401 match self {
402 Self::Collected(_) => { }
403 Self::Reader(reader) => {
404 let mut buf = Vec::new();
405 reader.read_to_end(&mut buf).await?;
406 *self = Self::Collected(buf);
407 }
408 }
409 let Self::Collected(bytes) = self else { unreachable!("just set to Collected") };
410 Ok(bytes)
411 }
412}
413
414fn header_packet_fits(
416 header: &proto::data_stream::Header,
417 destinations: &[ParticipantIdentity],
418) -> bool {
419 use prost::Message;
420 let packet = RawStream::create_header_packet(header.clone(), destinations.to_vec());
421 packet.encoded_len() <= constants::STREAM_CHUNK_SIZE_BYTES
422}
423
424fn enforce_header_size(header: &Header, destinations: &[ParticipantIdentity]) -> StreamResult<()> {
427 let proto_header: proto::data_stream::Header = header.clone().into();
428 if header_packet_fits(&proto_header, destinations) {
429 Ok(())
430 } else {
431 Err(StreamError::HeaderTooLarge)
432 }
433}
434
435fn build_text_header(
436 options: &StreamTextOptions,
437 stream_id: StreamId,
438 total_length: Option<u64>,
439 inline_content: Option<Vec<u8>>,
440 compression: CompressionType,
441) -> (Header, TextHeader) {
442 let text_header = TextHeader {
443 operation_type: options.operation_type.unwrap_or_default(),
444 version: options.version.unwrap_or_default(),
445 reply_to_stream_id: options.reply_to_stream_id.clone().map(StreamId::from),
446 attached_stream_ids: options
447 .attached_stream_ids
448 .clone()
449 .into_iter()
450 .map(StreamId::from)
451 .collect(),
452 generated: options.generated.unwrap_or_default(),
453 };
454 let header = Header {
455 stream_id,
456 timestamp: Utc::now().timestamp_millis(),
457 topic: options.topic.clone(),
458 mime_type: constants::TEXT_MIME_TYPE.to_owned(),
459 total_length,
460 attributes: options.attributes.clone(),
461 content_header: Some(ContentHeader::TextHeader(text_header.clone().into())),
462 inline_content,
463 compression,
464 };
465 (header, text_header)
466}
467
468fn build_byte_header(
469 options: &StreamByteOptions,
470 stream_id: StreamId,
471 name: String,
472 total_length: Option<u64>,
473 inline_content: Option<Vec<u8>>,
474 compression: CompressionType,
475) -> (Header, ByteHeader) {
476 let byte_header = ByteHeader { name };
477 let header = Header {
478 stream_id,
479 timestamp: Utc::now().timestamp_millis(),
480 topic: options.topic.clone(),
481 mime_type: options
482 .mime_type
483 .clone()
484 .unwrap_or_else(|| constants::BYTE_MIME_TYPE.to_owned()),
485 total_length,
486 attributes: options.attributes.clone(),
487 content_header: Some(byte_header.clone().into()),
488 inline_content,
489 compression,
490 };
491 (header, byte_header)
492}
493
494#[cfg(test)]
495mod tests {
496 use super::*;
497 use crate::{backend::somewhat_compressible, outgoing::StreamWriter};
498 use livekit_common::{CLIENT_PROTOCOL_DATA_STREAM_RPC, CLIENT_PROTOCOL_DEFAULT};
499 use std::{collections::HashMap, sync::Mutex as StdMutex};
500
501 struct FakeRegistry {
504 remotes: HashMap<String, (i32, Vec<ClientCapability>)>,
505 }
506
507 impl FakeRegistry {
508 fn new() -> Self {
509 Self { remotes: HashMap::new() }
510 }
511
512 fn add(mut self, id: &str, client_protocol: i32, caps: &[ClientCapability]) -> Self {
513 self.remotes.insert(id.to_string(), (client_protocol, caps.to_vec()));
514 self
515 }
516 }
517
518 impl RemoteParticipantRegistry for FakeRegistry {
519 fn remote_client_protocol(&self, identity: &ParticipantIdentity) -> i32 {
520 self.remotes.get(&identity.0).map(|(p, _)| *p).unwrap_or(0)
521 }
522 fn remote_capabilities(&self, identity: &ParticipantIdentity) -> Vec<ClientCapability> {
523 self.remotes.get(&identity.0).map(|(_, c)| c.clone()).unwrap_or_default()
524 }
525 fn remote_identities(&self) -> Vec<ParticipantIdentity> {
526 self.remotes.keys().map(|k| ParticipantIdentity(k.clone())).collect()
527 }
528 }
529
530 fn pre_v2_room() -> FakeRegistry {
531 FakeRegistry::new()
532 .add("alice", CLIENT_PROTOCOL_DEFAULT, &[])
533 .add("bob", CLIENT_PROTOCOL_DEFAULT, &[])
534 .add("jim", CLIENT_PROTOCOL_DATA_STREAM_RPC, &[])
535 }
536
537 fn all_v2_room() -> FakeRegistry {
538 FakeRegistry::new()
539 .add(
540 "alice",
541 CLIENT_PROTOCOL_DATA_STREAM_V2,
542 &[ClientCapability::CompressionDeflateRaw],
543 )
544 .add("bob", CLIENT_PROTOCOL_DATA_STREAM_V2, &[ClientCapability::CompressionDeflateRaw])
545 .add("noCompression", CLIENT_PROTOCOL_DATA_STREAM_V2, &[])
546 }
547
548 fn mixed_room() -> FakeRegistry {
549 FakeRegistry::new()
550 .add("alice", CLIENT_PROTOCOL_DEFAULT, &[])
551 .add("bob", CLIENT_PROTOCOL_DATA_STREAM_V2, &[ClientCapability::CompressionDeflateRaw])
552 .add("jim", CLIENT_PROTOCOL_DATA_STREAM_V2, &[ClientCapability::CompressionDeflateRaw])
553 .add("mallory", CLIENT_PROTOCOL_DEFAULT, &[])
554 .add("noCompression", CLIENT_PROTOCOL_DATA_STREAM_V2, &[])
555 }
556
557 fn all_v2_capable_room() -> FakeRegistry {
559 FakeRegistry::new()
560 .add(
561 "alice",
562 CLIENT_PROTOCOL_DATA_STREAM_V2,
563 &[ClientCapability::CompressionDeflateRaw],
564 )
565 .add("bob", CLIENT_PROTOCOL_DATA_STREAM_V2, &[ClientCapability::CompressionDeflateRaw])
566 }
567
568 type Sent = Arc<StdMutex<Vec<proto::DataPacket>>>;
571 type SentBatches = Arc<StdMutex<Vec<Vec<proto::DataPacket>>>>;
572
573 fn setup() -> (Manager, Sent) {
574 let (manager, mut packet_rx) = Manager::new();
575 let sent: Sent = Arc::new(StdMutex::new(Vec::new()));
576 let sink = sent.clone();
577 tokio::spawn(async move {
578 while let Ok((packets, responder)) = packet_rx.recv().await {
579 sink.lock().unwrap().extend(packets);
580 let _ = responder.respond(Ok(()));
581 }
582 });
583 (manager, sent)
584 }
585
586 fn setup_batched() -> (Manager, SentBatches) {
588 let (manager, mut packet_rx) = Manager::new();
589 let sent: SentBatches = Arc::new(StdMutex::new(Vec::new()));
590 let sink = sent.clone();
591 tokio::spawn(async move {
592 while let Ok((packets, responder)) = packet_rx.recv().await {
593 sink.lock().unwrap().push(packets);
594 let _ = responder.respond(Ok(()));
595 }
596 });
597 (manager, sent)
598 }
599
600 fn ids(list: &[&str]) -> Vec<ParticipantIdentity> {
601 list.iter().map(|s| ParticipantIdentity(s.to_string())).collect()
602 }
603
604 fn text_opts(topic: &str, dests: &[&str]) -> StreamTextOptions {
605 StreamTextOptions::new_with_topic(topic).with_destination_identities(ids(dests))
606 }
607
608 fn byte_opts(topic: &str, dests: &[&str]) -> StreamByteOptions {
609 StreamByteOptions::new_with_topic(topic).with_destination_identities(ids(dests))
610 }
611
612 fn header(p: &proto::DataPacket) -> &proto::data_stream::Header {
613 match p.value.as_ref().unwrap() {
614 proto::data_packet::Value::StreamHeader(h) => h,
615 _ => panic!("expected stream header"),
616 }
617 }
618
619 fn chunk(p: &proto::DataPacket) -> &proto::data_stream::Chunk {
620 match p.value.as_ref().unwrap() {
621 proto::data_packet::Value::StreamChunk(c) => c,
622 _ => panic!("expected stream chunk"),
623 }
624 }
625
626 fn is_text_header(h: &proto::data_stream::Header) -> bool {
627 matches!(h.content_header, Some(proto::data_stream::header::ContentHeader::TextHeader(_)))
628 }
629
630 fn is_byte_header(h: &proto::data_stream::Header) -> bool {
631 matches!(h.content_header, Some(proto::data_stream::header::ContentHeader::ByteHeader(_)))
632 }
633
634 fn assert_trailer(p: &proto::DataPacket) {
635 match p.value.as_ref().unwrap() {
636 proto::data_packet::Value::StreamTrailer(t) => assert_eq!(t.reason, ""),
637 _ => panic!("expected stream trailer"),
638 }
639 }
640
641 fn random_bytes(len: usize) -> Vec<u8> {
643 use rand::{rngs::StdRng, Rng, SeedableRng};
644 let mut rng = StdRng::seed_from_u64(0xdead_beef);
645 (0..len).map(|_| rng.random::<u8>()).collect()
646 }
647
648 fn text_content_header(h: &proto::data_stream::Header) -> &proto::data_stream::TextHeader {
649 match h.content_header.as_ref().unwrap() {
650 proto::data_stream::header::ContentHeader::TextHeader(t) => t,
651 _ => panic!("expected text header"),
652 }
653 }
654
655 mod room_with_pre_data_streams_v2_participants {
656 use super::*;
657
658 #[tokio::test]
659 async fn pre_v2_short_text_is_legacy_multipacket() {
660 let (m, sent) = setup();
661 m.send_text("hello world", text_opts("chat", &[]), &pre_v2_room()).await.unwrap();
662 let p = sent.lock().unwrap().clone();
663 assert_eq!(p.len(), 3);
664 let h = header(&p[0]);
665 assert!(is_text_header(h));
666 assert_eq!(h.topic, "chat");
667 assert_eq!(h.compression(), proto::data_stream::CompressionType::None);
668 assert!(h.inline_content.is_none());
669 let c = chunk(&p[1]);
670 assert_eq!(c.chunk_index, 0);
671 assert_eq!(c.content, b"hello world");
672 assert_trailer(&p[2]);
673 }
674
675 #[tokio::test]
676 async fn pre_v2_long_text_splits_at_mtu() {
677 let (m, sent) = setup();
678 let text = "A".repeat(40_000);
679 m.send_text(&text, text_opts("chat", &[]), &pre_v2_room()).await.unwrap();
680 let p = sent.lock().unwrap().clone();
681 assert_eq!(p.len(), 5); assert_eq!(header(&p[0]).compression(), proto::data_stream::CompressionType::None);
683 assert_eq!(chunk(&p[1]).content.len(), 15_000);
684 assert_eq!(chunk(&p[2]).content.len(), 15_000);
685 assert_eq!(chunk(&p[3]).content.len(), 10_000);
686 assert_eq!(chunk(&p[1]).chunk_index, 0);
687 assert_eq!(chunk(&p[3]).chunk_index, 2);
688 assert_trailer(&p[4]);
689 }
690
691 #[tokio::test]
692 async fn pre_v2_bytes_is_legacy_multipacket() {
693 let (m, sent) = setup();
694 m.send_bytes([0u8, 1, 2, 3], byte_opts("blob", &[]), &pre_v2_room()).await.unwrap();
695 let p = sent.lock().unwrap().clone();
696 assert_eq!(p.len(), 3);
697 let h = header(&p[0]);
698 assert!(is_byte_header(h));
699 assert_eq!(h.compression(), proto::data_stream::CompressionType::None);
700 assert!(h.inline_content.is_none());
701 assert_eq!(chunk(&p[1]).content, vec![0, 1, 2, 3]);
702 assert_trailer(&p[2]);
703 }
704
705 #[tokio::test]
706 async fn pre_v2_empty_text_sends_header_and_trailer() {
707 let (m, sent) = setup();
708 m.send_text("", text_opts("chat", &[]), &pre_v2_room()).await.unwrap();
709 let p = sent.lock().unwrap().clone();
711 assert_eq!(p.len(), 2);
712 assert_eq!(header(&p[0]).total_length, Some(0));
713 assert_trailer(&p[1]);
714 }
715 }
716
717 mod room_with_all_data_streams_v2_participants {
718 use super::*;
719
720 mod send_text {
721 use super::*;
722
723 #[tokio::test]
724 async fn v2_short_compressible_text_inlines_compressed() {
725 let (m, sent) = setup();
726 let text = "hello hello compressible world";
727 m.send_text(text, text_opts("chat", &["alice", "bob"]), &all_v2_room())
728 .await
729 .unwrap();
730 let p = sent.lock().unwrap().clone();
731 assert_eq!(p.len(), 1);
732 let h = header(&p[0]);
733 assert!(is_text_header(h));
734 assert_eq!(h.compression(), proto::data_stream::CompressionType::DeflateRaw);
735 let inline = h.inline_content.as_ref().unwrap();
736 assert_ne!(inline.as_slice(), text.as_bytes()); }
738
739 #[tokio::test]
740 async fn v2_short_incompressible_text_inlines_raw() {
741 let (m, sent) = setup();
742 m.send_text("short", text_opts("chat", &["alice", "bob"]), &all_v2_room())
743 .await
744 .unwrap();
745 let p = sent.lock().unwrap().clone();
746 assert_eq!(p.len(), 1);
747 let h = header(&p[0]);
748 assert_eq!(h.compression(), proto::data_stream::CompressionType::None);
749 assert_eq!(h.inline_content.as_ref().unwrap().as_slice(), b"short");
750 }
751
752 #[tokio::test]
753 async fn v2_no_compression_cap_inlines_raw() {
754 let (m, sent) = setup();
755 let text = "hello hello compressible world";
756 m.send_text(text, text_opts("chat", &["noCompression"]), &all_v2_room())
757 .await
758 .unwrap();
759 let p = sent.lock().unwrap().clone();
760 assert_eq!(p.len(), 1); let h = header(&p[0]);
762 assert_eq!(h.compression(), proto::data_stream::CompressionType::None); assert_eq!(h.inline_content.as_ref().unwrap().as_slice(), text.as_bytes());
764 }
765
766 #[tokio::test]
767 async fn v2_large_highly_compressible_text_still_inlines() {
768 let (m, sent) = setup();
769 let text = "hello world".repeat(20_000);
770 m.send_text(&text, text_opts("chat", &["alice", "bob"]), &all_v2_room())
771 .await
772 .unwrap();
773 let p = sent.lock().unwrap().clone();
774 assert_eq!(p.len(), 1);
775 let h = header(&p[0]);
776 assert_eq!(h.compression(), proto::data_stream::CompressionType::DeflateRaw);
777 assert!(h.inline_content.as_ref().unwrap().len() < text.len());
778 }
779
780 #[tokio::test]
781 async fn v2_somewhat_compressible_text_is_compressed_multipacket() {
782 let (m, sent) = setup();
783 let text = somewhat_compressible(50_000);
784 m.send_text(&text, text_opts("chat", &["alice", "bob"]), &all_v2_room())
785 .await
786 .unwrap();
787 let p = sent.lock().unwrap().clone();
788 let h = header(&p[0]);
789 assert_eq!(h.compression(), proto::data_stream::CompressionType::DeflateRaw);
790 assert!(h.inline_content.is_none());
791 let chunks: Vec<_> = p[1..p.len() - 1].iter().map(chunk).collect();
792 let uncompressed_chunks = text.len().div_ceil(constants::STREAM_CHUNK_SIZE_BYTES);
794 assert!(chunks.len() >= 2);
795 assert!(chunks.len() < uncompressed_chunks);
796 assert_eq!(chunks[0].content.len(), constants::STREAM_CHUNK_SIZE_BYTES); let total: usize = chunks.iter().map(|c| c.content.len()).sum();
798 assert!(total < text.len()); assert_trailer(p.last().unwrap());
800 }
801
802 #[tokio::test]
803 async fn v2_compress_false_short_inlines_raw() {
804 let (m, sent) = setup();
805 let text = "hello hello compressible world";
806 let opts = text_opts("chat", &["alice", "bob"]).with_compress(false);
807 m.send_text(text, opts, &all_v2_room()).await.unwrap();
808 let p = sent.lock().unwrap().clone();
809 assert_eq!(p.len(), 1);
810 let h = header(&p[0]);
811 assert_eq!(h.compression(), proto::data_stream::CompressionType::None);
812 assert_eq!(h.inline_content.as_ref().unwrap().as_slice(), text.as_bytes());
813 }
814
815 #[tokio::test]
816 async fn v2_compress_false_large_is_uncompressed_multipacket() {
817 let (m, sent) = setup();
818 let text = "B".repeat(50_000);
819 let opts = text_opts("chat", &["alice", "bob"]).with_compress(false);
820 m.send_text(&text, opts, &all_v2_room()).await.unwrap();
821 let p = sent.lock().unwrap().clone();
822 assert_eq!(p.len(), 6); assert_eq!(header(&p[0]).compression(), proto::data_stream::CompressionType::None);
824 assert_eq!(chunk(&p[1]).content.len(), 15_000);
825 }
826
827 #[tokio::test]
828 async fn v2_send_text_with_attachments_never_inlines() {
829 let (m, sent) = setup();
830 let opts = text_opts("chat", &["alice", "bob"]).with_attached_stream_id("att1");
831 m.send_text("hello hello compressible world", opts, &all_v2_room()).await.unwrap();
832 let p = sent.lock().unwrap().clone();
833 assert_eq!(p.len(), 3); let h = header(&p[0]);
836 assert!(h.inline_content.is_none());
837 assert_eq!(text_content_header(h).attached_stream_ids, vec!["att1".to_string()]);
838 assert_trailer(&p[2]);
839 }
840
841 #[tokio::test]
842 async fn v2_large_text_to_uncapable_recipient_is_uncompressed_multipacket() {
843 let (m, sent) = setup();
844 let text = "A".repeat(40_000);
845 m.send_text(&text, text_opts("chat", &["noCompression"]), &all_v2_room())
846 .await
847 .unwrap();
848 let p = sent.lock().unwrap().clone();
852 assert_eq!(p.len(), 5);
853 let h = header(&p[0]);
854 assert_eq!(h.compression(), proto::data_stream::CompressionType::None);
855 assert!(h.inline_content.is_none());
856 assert_eq!(chunk(&p[1]).content.len(), 15_000);
857 assert_eq!(chunk(&p[2]).content.len(), 15_000);
858 assert_eq!(chunk(&p[3]).content.len(), 10_000);
859 assert_trailer(&p[4]);
860 }
861
862 #[tokio::test]
863 async fn v2_empty_text_sends_single_inline_packet() {
864 let (m, sent) = setup();
865 m.send_text("", text_opts("chat", &["alice", "bob"]), &all_v2_room())
866 .await
867 .unwrap();
868 let p = sent.lock().unwrap().clone();
869 assert_eq!(p.len(), 1);
870 let h = header(&p[0]);
871 assert_eq!(h.compression(), proto::data_stream::CompressionType::None);
873 assert_eq!(h.inline_content.as_deref(), Some(&[][..]));
874 assert_eq!(h.total_length, Some(0));
875 }
876 }
877
878 mod send_bytes {
879 use super::*;
880
881 #[tokio::test]
882 async fn v2_send_bytes_short_incompressible_inlines_raw() {
883 let (m, sent) = setup();
884 m.send_bytes([0u8, 1, 2, 3], byte_opts("blob", &["alice", "bob"]), &all_v2_room())
885 .await
886 .unwrap();
887 let p = sent.lock().unwrap().clone();
888 assert_eq!(p.len(), 1);
889 let h = header(&p[0]);
890 assert_eq!(h.compression(), proto::data_stream::CompressionType::None);
892 assert_eq!(h.inline_content.as_ref().unwrap().as_slice(), &[0u8, 1, 2, 3]);
893 }
894
895 #[tokio::test]
896 async fn v2_send_bytes_no_compression_cap_inlines_raw() {
897 let (m, sent) = setup();
898 let payload = "hello hello compressible world".as_bytes();
899 m.send_bytes(payload, byte_opts("blob", &["noCompression"]), &all_v2_room())
900 .await
901 .unwrap();
902 let p = sent.lock().unwrap().clone();
903 assert_eq!(p.len(), 1); let h = header(&p[0]);
905 assert_eq!(h.compression(), proto::data_stream::CompressionType::None); assert_eq!(h.inline_content.as_ref().unwrap().as_slice(), payload);
907 }
908
909 #[tokio::test]
910 async fn v2_send_bytes_large_highly_compressible_inlines() {
911 let (m, sent) = setup();
912 let payload = vec![0x01u8; 50_000];
913 m.send_bytes(&payload, byte_opts("blob", &["alice", "bob"]), &all_v2_room())
914 .await
915 .unwrap();
916 let p = sent.lock().unwrap().clone();
917 assert_eq!(p.len(), 1); let h = header(&p[0]);
919 assert_eq!(h.compression(), proto::data_stream::CompressionType::DeflateRaw);
920 assert!(h.inline_content.as_ref().unwrap().len() < payload.len());
921 assert_eq!(h.total_length, Some(50_000)); }
923
924 #[tokio::test]
925 async fn v2_send_bytes_somewhat_compressible_is_compressed_multipacket() {
926 let (m, sent) = setup();
927 let payload = somewhat_compressible(50_000).into_bytes();
928 m.send_bytes(&payload, byte_opts("blob", &["alice", "bob"]), &all_v2_room())
929 .await
930 .unwrap();
931 let p = sent.lock().unwrap().clone();
932 let h = header(&p[0]);
933 assert!(is_byte_header(h));
934 assert_eq!(h.compression(), proto::data_stream::CompressionType::DeflateRaw);
935 assert!(h.inline_content.is_none());
936 let chunks: Vec<_> = p[1..p.len() - 1].iter().map(chunk).collect();
937 assert!(chunks.len() >= 2);
938 assert!(chunks.len() < payload.len().div_ceil(constants::STREAM_CHUNK_SIZE_BYTES));
939 assert_eq!(chunks[0].content.len(), constants::STREAM_CHUNK_SIZE_BYTES);
940 assert_trailer(p.last().unwrap());
941 }
942
943 #[tokio::test]
944 async fn v2_send_bytes_compress_false_large_is_uncompressed_multipacket() {
945 let (m, sent) = setup();
946 let payload = vec![0x07u8; 40_000];
947 let opts = byte_opts("blob", &["alice", "bob"]).with_compress(false);
948 m.send_bytes(&payload, opts, &all_v2_room()).await.unwrap();
949 let p = sent.lock().unwrap().clone();
950 assert_eq!(p.len(), 5); let h = header(&p[0]);
952 assert_eq!(h.compression(), proto::data_stream::CompressionType::None);
953 assert!(h.inline_content.is_none());
954 assert_eq!(chunk(&p[1]).content.len(), 15_000);
955 assert_eq!(chunk(&p[2]).content.len(), 15_000);
956 assert_eq!(chunk(&p[3]).content.len(), 10_000);
957 assert!(chunk(&p[3]).content.iter().all(|b| *b == 0x07));
958 assert_trailer(&p[4]);
959 }
960
961 #[tokio::test]
962 async fn v2_send_bytes_short_compressible_inlines_compressed() {
963 let (m, sent) = setup();
964 let payload = "hello hello compressible world".as_bytes().to_vec();
965 let mut opts = byte_opts("blob", &["alice", "bob"]);
966 opts.attributes.insert("foo".to_string(), "bar".to_string());
967 let info = m.send_bytes(&payload, opts, &all_v2_room()).await.unwrap();
968 let p = sent.lock().unwrap().clone();
969 assert_eq!(p.len(), 1);
970 let h = header(&p[0]);
971 assert!(is_byte_header(h));
972 assert_eq!(h.compression(), proto::data_stream::CompressionType::DeflateRaw);
973 assert_ne!(h.inline_content.as_ref().unwrap().as_slice(), payload.as_slice());
974 assert_eq!(info.name, "unknown");
975 assert_eq!(info.mime_type, "application/octet-stream");
976 assert_eq!(info.total_length, Some(payload.len() as u64));
977 assert_eq!(info.attributes().get("foo"), Some(&"bar".to_string()));
978 }
979 }
980
981 mod send_file {
982 use super::*;
983
984 async fn write_temp_file(bytes: &[u8]) -> std::path::PathBuf {
985 let path =
986 std::env::temp_dir().join(format!("lk_ds_test_{}.bin", create_random_uuid()));
987 tokio::fs::write(&path, bytes).await.unwrap();
988 path
989 }
990
991 #[tokio::test]
992 async fn send_file_never_inlines_and_compresses_when_eligible() {
993 let (m, sent) = setup();
994 let path = write_temp_file(&vec![0x01u8; 10_000]).await;
995 m.send_file(&path, byte_opts("file", &["alice", "bob"]), &all_v2_room())
996 .await
997 .unwrap();
998 let _ = tokio::fs::remove_file(&path).await;
999 let p = sent.lock().unwrap().clone();
1000 assert_eq!(p.len(), 3); let h = header(&p[0]);
1002 assert!(is_byte_header(h));
1003 assert_eq!(h.compression(), proto::data_stream::CompressionType::DeflateRaw);
1004 assert!(h.inline_content.is_none());
1005 assert!(chunk(&p[1]).content.len() < 10_000); assert_trailer(&p[2]);
1007 }
1008
1009 #[tokio::test]
1010 async fn send_file_uncompressed_splits_at_mtu() {
1011 let (m, sent) = setup();
1012 let path = write_temp_file(&vec![0x07u8; 20_000]).await;
1013 m.send_file(&path, byte_opts("file", &[]).with_compress(false), &all_v2_room())
1014 .await
1015 .unwrap();
1016 let _ = tokio::fs::remove_file(&path).await;
1017 let p = sent.lock().unwrap().clone();
1018 assert_eq!(p.len(), 4); assert_eq!(header(&p[0]).compression(), proto::data_stream::CompressionType::None);
1020 assert_eq!(chunk(&p[1]).content.len(), 15_000);
1021 assert_eq!(chunk(&p[2]).content.len(), 5_000);
1022 assert_eq!(chunk(&p[2]).chunk_index, 1);
1023 assert_trailer(&p[3]);
1024 }
1025
1026 #[tokio::test]
1027 async fn send_file_incompressible_compressed_expands() {
1028 let (m, sent) = setup();
1029 let data = random_bytes(50_000);
1030 let path = write_temp_file(&data).await;
1031 m.send_file(&path, byte_opts("file", &["alice", "bob"]), &all_v2_room())
1032 .await
1033 .unwrap();
1034 let _ = tokio::fs::remove_file(&path).await;
1035
1036 let p = sent.lock().unwrap().clone();
1037 assert_eq!(p.len(), 6); let h = header(&p[0]);
1039 assert_eq!(h.compression(), proto::data_stream::CompressionType::DeflateRaw);
1040 assert_eq!(chunk(&p[1]).content.len(), 15_000);
1041 let total: usize =
1044 p[1..p.len() - 1].iter().map(|packet| chunk(packet).content.len()).sum();
1045 assert!(total > data.len());
1046 assert_trailer(p.last().unwrap());
1047 }
1048
1049 #[tokio::test]
1050 async fn send_file_to_no_compression_recipient_is_uncompressed() {
1051 let (m, sent) = setup();
1052 let path = write_temp_file(&vec![0x07u8; 10_000]).await;
1053 m.send_file(&path, byte_opts("file", &["noCompression"]), &all_v2_room())
1054 .await
1055 .unwrap();
1056 let _ = tokio::fs::remove_file(&path).await;
1057
1058 let p = sent.lock().unwrap().clone();
1061 assert_eq!(p.len(), 3);
1062 let h = header(&p[0]);
1063 assert!(is_byte_header(h));
1064 assert_eq!(h.compression(), proto::data_stream::CompressionType::None);
1065 assert!(h.inline_content.is_none());
1066 let c = chunk(&p[1]);
1067 assert_eq!(c.content.len(), 10_000);
1068 assert!(c.content.iter().all(|b| *b == 0x07));
1069 assert_trailer(&p[2]);
1070 }
1071
1072 #[tokio::test]
1073 async fn send_file_empty_file() {
1074 let (m, sent) = setup();
1075 let path = write_temp_file(&[]).await;
1076 m.send_file(&path, byte_opts("file", &["alice", "bob"]), &all_v2_room())
1077 .await
1078 .unwrap();
1079 let _ = tokio::fs::remove_file(&path).await;
1080
1081 let p = sent.lock().unwrap().clone();
1084 let h = header(&p[0]);
1085 assert!(is_byte_header(h));
1086 assert_eq!(h.total_length, Some(0));
1087 assert_eq!(h.compression(), proto::data_stream::CompressionType::DeflateRaw);
1088 assert_trailer(p.last().unwrap());
1089 }
1090 }
1091
1092 #[tokio::test]
1093 async fn v2_broadcast_all_capable_inlines_compressed() {
1094 let (m, sent) = setup();
1095 m.send_text(
1097 "hello hello compressible world",
1098 text_opts("chat", &[]),
1099 &all_v2_capable_room(),
1100 )
1101 .await
1102 .unwrap();
1103 let p = sent.lock().unwrap().clone();
1104 assert_eq!(p.len(), 1);
1105 assert_eq!(
1106 header(&p[0]).compression(),
1107 proto::data_stream::CompressionType::DeflateRaw
1108 );
1109 }
1110
1111 #[tokio::test]
1112 async fn v2_broadcast_with_uncapable_member_inlines_raw() {
1113 let (m, sent) = setup();
1114 let text = "hello hello compressible world";
1115 m.send_text(text, text_opts("chat", &[]), &all_v2_room()).await.unwrap();
1117 let p = sent.lock().unwrap().clone();
1118 assert_eq!(p.len(), 1);
1119 let h = header(&p[0]);
1120 assert_eq!(h.compression(), proto::data_stream::CompressionType::None);
1121 assert_eq!(h.inline_content.as_ref().unwrap().as_slice(), text.as_bytes());
1122 }
1123
1124 #[tokio::test]
1125 async fn empty_room_broadcast_is_v2_eligible() {
1126 let (m, sent) = setup();
1127 m.send_text(
1129 "hello hello compressible world",
1130 text_opts("chat", &[]),
1131 &FakeRegistry::new(),
1132 )
1133 .await
1134 .unwrap();
1135 let p = sent.lock().unwrap().clone();
1136 assert_eq!(p.len(), 1);
1137 assert_eq!(
1138 header(&p[0]).compression(),
1139 proto::data_stream::CompressionType::DeflateRaw
1140 );
1141 }
1142 }
1143
1144 mod room_with_mixed_participants {
1145 use super::*;
1146
1147 #[tokio::test]
1148 async fn mixed_broadcast_falls_back_to_legacy() {
1149 let (m, sent) = setup();
1150 m.send_text("hello world", text_opts("chat", &[]), &mixed_room()).await.unwrap();
1151 let p = sent.lock().unwrap().clone();
1152 assert_eq!(p.len(), 3);
1153 assert_eq!(header(&p[0]).compression(), proto::data_stream::CompressionType::None);
1154 assert!(header(&p[0]).inline_content.is_none());
1155 assert_eq!(chunk(&p[1]).content, b"hello world");
1156 }
1157
1158 #[tokio::test]
1159 async fn mixed_targeted_v2_subset_inlines_compressed() {
1160 let (m, sent) = setup();
1161 let text = "hello hello compressible world";
1162 m.send_text(text, text_opts("chat", &["bob", "jim"]), &mixed_room()).await.unwrap();
1163 let p = sent.lock().unwrap().clone();
1164 assert_eq!(p.len(), 1);
1165 let h = header(&p[0]);
1166 assert_eq!(h.compression(), proto::data_stream::CompressionType::DeflateRaw);
1167 assert_ne!(h.inline_content.as_ref().unwrap().as_slice(), text.as_bytes());
1168 }
1169
1170 #[tokio::test]
1171 async fn mixed_targeted_subset_missing_cap_inlines_uncompressed() {
1172 let (m, sent) = setup();
1173 let text = "hello hello compressible world";
1174 m.send_text(text, text_opts("chat", &["bob", "jim", "noCompression"]), &mixed_room())
1175 .await
1176 .unwrap();
1177 let p = sent.lock().unwrap().clone();
1178 assert_eq!(p.len(), 1);
1179 let h = header(&p[0]);
1180 assert_eq!(h.compression(), proto::data_stream::CompressionType::None);
1181 assert_eq!(h.inline_content.as_ref().unwrap().as_slice(), text.as_bytes());
1182 }
1183 }
1184
1185 #[tokio::test]
1188 async fn stream_text_with_sender_identity_stamps_every_packet() {
1189 let (m, sent) = setup();
1190 let opts = text_opts("chat", &[]).with_sender_identity("impostor");
1191 let writer = m.stream_text(opts).await.unwrap();
1192 writer.write("hello").await.unwrap();
1193 writer.close().await.unwrap();
1194 let p = sent.lock().unwrap().clone();
1195 assert_eq!(p.len(), 3);
1196 assert!(p.iter().all(|pkt| pkt.participant_identity == "impostor"));
1197 }
1198
1199 #[tokio::test]
1200 async fn send_text_inline_with_sender_identity_stamps_packet() {
1201 let (m, sent) = setup();
1202 let opts = text_opts("chat", &["alice", "bob"]).with_sender_identity("impostor");
1203 m.send_text("hello hello compressible world", opts, &all_v2_room()).await.unwrap();
1204 let p = sent.lock().unwrap().clone();
1205 assert_eq!(p.len(), 1);
1206 assert_eq!(p[0].participant_identity, "impostor");
1207 }
1208
1209 #[tokio::test]
1210 async fn packets_carry_no_identity_when_sender_identity_unset() {
1211 let (m, sent) = setup();
1212 let writer = m.stream_text(text_opts("chat", &[])).await.unwrap();
1213 writer.close().await.unwrap();
1214 let p = sent.lock().unwrap().clone();
1215 assert!(p.iter().all(|pkt| pkt.participant_identity.is_empty()));
1216 }
1217
1218 #[tokio::test]
1224 async fn one_shot_send_text_targets_every_packet() {
1225 let (m, sent) = setup();
1226 let text = "A".repeat(20_000);
1227 m.send_text(&text, text_opts("chat", &["alice", "bob"]), &pre_v2_room()).await.unwrap();
1228 let p = sent.lock().unwrap().clone();
1229 assert_eq!(p.len(), 4); assert!(p.iter().all(|pkt| pkt.destination_identities == ["alice", "bob"]));
1231 }
1232
1233 #[tokio::test]
1234 async fn stream_text_targets_every_packet() {
1235 let (m, sent) = setup();
1236 let writer = m.stream_text(text_opts("chat", &["alice"])).await.unwrap();
1237 writer.write("hello").await.unwrap();
1238 writer.close().await.unwrap();
1239 let p = sent.lock().unwrap().clone();
1240 assert_eq!(p.len(), 3);
1241 assert!(p.iter().all(|pkt| pkt.destination_identities == ["alice"]));
1242 }
1243
1244 #[tokio::test]
1245 async fn send_file_targets_every_packet() {
1246 let (m, sent) = setup();
1247 let path = std::env::temp_dir().join(format!("lk_ds_dest_{}.bin", create_random_uuid()));
1248 tokio::fs::write(&path, vec![0x07u8; 20_000]).await.unwrap();
1249 let opts = byte_opts("file", &["bob"]).with_compress(false);
1250 m.send_file(&path, opts, &all_v2_room()).await.unwrap();
1251 let _ = tokio::fs::remove_file(&path).await;
1252 let p = sent.lock().unwrap().clone();
1253 assert_eq!(p.len(), 4); assert!(p.iter().all(|pkt| pkt.destination_identities == ["bob"]));
1255 }
1256
1257 #[tokio::test]
1260 async fn stream_text_never_compresses_or_inlines() {
1261 let (m, sent) = setup();
1262 let writer = m.stream_text(text_opts("chat", &["noCompression"])).await.unwrap();
1263 assert_eq!(sent.lock().unwrap().len(), 1);
1264 let h0 = sent.lock().unwrap()[0].clone();
1265 assert!(is_text_header(header(&h0)));
1266 assert_eq!(header(&h0).compression(), proto::data_stream::CompressionType::None);
1267 assert!(header(&h0).inline_content.is_none());
1268
1269 writer.write("hello world").await.unwrap();
1270 assert_eq!(sent.lock().unwrap().len(), 2);
1271 assert_eq!(chunk(&sent.lock().unwrap()[1]).content, b"hello world");
1272
1273 writer.close().await.unwrap();
1274 let p = sent.lock().unwrap().clone();
1275 assert_eq!(p.len(), 3);
1276 assert_trailer(&p[2]);
1277 }
1278
1279 #[tokio::test]
1280 async fn stream_bytes_never_compresses_or_inlines() {
1281 let (m, sent) = setup();
1282 let writer = m.stream_bytes(byte_opts("blob", &["noCompression"])).await.unwrap();
1283 assert_eq!(sent.lock().unwrap().len(), 1);
1284 assert_eq!(
1285 header(&sent.lock().unwrap()[0]).compression(),
1286 proto::data_stream::CompressionType::None
1287 );
1288
1289 writer.write(&[0u8, 1, 2, 3]).await.unwrap();
1290 assert_eq!(chunk(&sent.lock().unwrap()[1]).content, vec![0, 1, 2, 3]);
1291
1292 writer.close().await.unwrap();
1293 let p = sent.lock().unwrap().clone();
1294 assert_eq!(p.len(), 3);
1295 assert_trailer(&p[2]);
1296 }
1297
1298 mod header_size_limit {
1299 use super::*;
1300
1301 #[tokio::test]
1302 async fn oversized_attributes_on_chunked_path_errors() {
1303 let (m, _sent) = setup();
1304 let mut opts = text_opts("chat", &[]); opts.attributes.insert("big".to_string(), "x".repeat(20_000));
1306 let result = m.send_text("hello", opts, &pre_v2_room()).await;
1307 assert!(matches!(result, Err(StreamError::HeaderTooLarge)));
1308 }
1309
1310 fn header_with_attributes(attributes: HashMap<String, String>) -> Header {
1312 Header {
1313 stream_id: "s1".into(),
1314 timestamp: 0,
1315 topic: "chat".to_string(),
1316 mime_type: constants::TEXT_MIME_TYPE.to_owned(),
1317 total_length: None,
1318 attributes,
1319 content_header: Some(ContentHeader::TextHeader(TextHeader::default())),
1320 inline_content: None,
1321 compression: CompressionType::None,
1322 }
1323 }
1324
1325 #[test]
1326 fn enforce_header_size_accepts_small_header() {
1327 let header = header_with_attributes(HashMap::new());
1329 assert!(enforce_header_size(&header, &[]).is_ok());
1330 }
1331
1332 #[test]
1333 fn enforce_header_size_rejects_large_header() {
1334 let mut attributes = HashMap::new();
1336 attributes.insert("big".to_string(), "x".repeat(20_000));
1337 let header = header_with_attributes(attributes);
1338 assert!(matches!(enforce_header_size(&header, &[]), Err(StreamError::HeaderTooLarge)));
1339 }
1340 }
1341
1342 #[test]
1346 fn drop_raw_stream_on_non_tokio_thread_does_not_panic() {
1347 let rt = tokio::runtime::Runtime::new().unwrap();
1348
1349 let raw_stream = rt.block_on(async {
1350 let (packet_tx, mut packet_rx) =
1351 bmrng::unbounded_channel::<Vec<proto::DataPacket>, Result<(), SendError>>();
1352
1353 tokio::spawn(async move {
1354 while let Ok((_packet, responder)) = packet_rx.recv().await {
1355 let _ = responder.respond(Ok(()));
1356 }
1357 });
1358
1359 let header = Header {
1360 stream_id: "gc-test-stream".into(),
1361 timestamp: 0,
1362 topic: "gc-test-topic".to_string(),
1363 mime_type: constants::TEXT_MIME_TYPE.to_owned(),
1364 total_length: None,
1365 attributes: HashMap::new(),
1366 content_header: None,
1367 inline_content: None,
1369 compression: CompressionType::None,
1370 };
1371
1372 RawStream::open(RawStreamOpenOptions {
1373 header,
1374 destination_identities: vec![],
1375 sender_identity: None,
1376 packet_tx,
1377 })
1378 .await
1379 .expect("RawStream should open")
1380 });
1381
1382 let drop_thread = std::thread::spawn(move || drop(raw_stream));
1383
1384 drop_thread.join().expect("Dropping RawStream on a non-Tokio thread must not panic");
1385 }
1386
1387 mod packet_batching {
1390 use super::*;
1391
1392 #[tokio::test]
1393 async fn one_shot_send_text_is_a_single_transport_request() {
1394 let (m, sent) = setup_batched();
1395 let text = "A".repeat(40_000);
1398 m.send_text(&text, text_opts("chat", &[]), &pre_v2_room()).await.unwrap();
1399 let batches = sent.lock().unwrap().clone();
1400 assert_eq!(batches.iter().map(Vec::len).collect::<Vec<_>>(), vec![5]);
1401 let batch = &batches[0];
1402 assert!(matches!(batch[0].value, Some(proto::data_packet::Value::StreamHeader(_))));
1403 for (i, packet) in batch[1..4].iter().enumerate() {
1404 assert_eq!(chunk(packet).chunk_index, i as u64);
1405 }
1406 assert_trailer(&batch[4]);
1407 }
1408
1409 #[tokio::test]
1410 async fn one_shot_send_bytes_is_a_single_transport_request() {
1411 let (m, sent) = setup_batched();
1412 let payload = vec![0x07u8; 40_000];
1413 let opts = byte_opts("blob", &["alice", "bob"]).with_compress(false);
1414 m.send_bytes(&payload, opts, &all_v2_room()).await.unwrap();
1415 let batches = sent.lock().unwrap().clone();
1416 assert_eq!(batches.iter().map(Vec::len).collect::<Vec<_>>(), vec![5]);
1417 }
1418
1419 #[tokio::test]
1420 async fn one_shot_send_with_sender_identity_stamps_every_packet() {
1421 let (m, sent) = setup_batched();
1422 let opts = text_opts("chat", &[]).with_sender_identity("impostor");
1423 m.send_text(&"A".repeat(20_000), opts, &pre_v2_room()).await.unwrap();
1424 let batches = sent.lock().unwrap().clone();
1425 assert_eq!(batches.iter().map(Vec::len).collect::<Vec<_>>(), vec![4]);
1426 assert!(batches[0].iter().all(|pkt| pkt.participant_identity == "impostor"));
1427 }
1428
1429 #[tokio::test]
1430 async fn incremental_writer_sends_per_write() {
1431 let (m, sent) = setup_batched();
1432 let writer = m.stream_text(text_opts("chat", &[])).await.unwrap();
1433 writer.write("hello").await.unwrap();
1434 writer.write("world").await.unwrap();
1435 writer.close().await.unwrap();
1436 let batches = sent.lock().unwrap().clone();
1438 assert_eq!(batches.iter().map(Vec::len).collect::<Vec<_>>(), vec![1, 1, 1, 1]);
1439 }
1440
1441 #[tokio::test]
1442 async fn send_file_streams_one_packet_per_request() {
1443 let (m, sent) = setup_batched();
1446 let path =
1447 std::env::temp_dir().join(format!("lk_ds_batch_{}.bin", create_random_uuid()));
1448 tokio::fs::write(&path, vec![0x07u8; 20_000]).await.unwrap();
1449 m.send_file(&path, byte_opts("file", &[]).with_compress(false), &all_v2_room())
1450 .await
1451 .unwrap();
1452 let _ = tokio::fs::remove_file(&path).await;
1453 let batches = sent.lock().unwrap().clone();
1454 assert_eq!(batches.iter().map(Vec::len).collect::<Vec<_>>(), vec![1, 1, 1, 1]);
1456 }
1457 }
1458
1459 mod stream_text_bytes {
1462 use super::*;
1463
1464 #[tokio::test]
1465 async fn stream_bytes_multi_write_splits_at_mtu() {
1466 let (m, sent) = setup();
1467 let writer = m.stream_bytes(byte_opts("blob", &[])).await.unwrap();
1468 writer.write(&vec![0x01u8; 20_000]).await.unwrap();
1469 writer.write(&vec![0x01u8; 20_000]).await.unwrap();
1470 writer.close().await.unwrap();
1471
1472 let p = sent.lock().unwrap().clone();
1474 assert_eq!(p.len(), 6); for (i, expected_len) in [15_000usize, 5_000, 15_000, 5_000].iter().enumerate() {
1476 let c = chunk(&p[i + 1]);
1477 assert_eq!(c.chunk_index, i as u64);
1478 assert_eq!(c.content.len(), *expected_len);
1479 assert!(c.content.iter().all(|b| *b == 0x01));
1480 }
1481 assert_trailer(&p[5]);
1482 }
1483
1484 #[tokio::test]
1485 async fn close_with_options_sends_trailer_attributes() {
1486 let (m, sent) = setup();
1487 let writer = m.stream_text(text_opts("chat", &[])).await.unwrap();
1488 writer.write("hello").await.unwrap();
1489 let attributes = HashMap::from([("result".to_string(), "ok".to_string())]);
1490 writer.close_with_options(None, Some(attributes.clone())).await.unwrap();
1491
1492 let p = sent.lock().unwrap().clone();
1493 let trailer = match p.last().unwrap().value.as_ref().unwrap() {
1494 proto::data_packet::Value::StreamTrailer(t) => t,
1495 _ => panic!("expected stream trailer"),
1496 };
1497 assert_eq!(trailer.reason, "");
1498 assert_eq!(trailer.attributes, attributes);
1499 }
1500
1501 #[tokio::test]
1502 async fn close_with_options_sends_reason_and_attributes() {
1503 let (m, sent) = setup();
1504 let writer = m.stream_text(text_opts("chat", &[])).await.unwrap();
1505 let attributes = HashMap::from([("cause".to_string(), "cancelled".to_string())]);
1506 writer.close_with_options(Some("aborted"), Some(attributes.clone())).await.unwrap();
1507
1508 let p = sent.lock().unwrap().clone();
1509 let trailer = match p.last().unwrap().value.as_ref().unwrap() {
1510 proto::data_packet::Value::StreamTrailer(t) => t,
1511 _ => panic!("expected stream trailer"),
1512 };
1513 assert_eq!(trailer.reason, "aborted");
1514 assert_eq!(trailer.attributes, attributes);
1515 }
1516
1517 #[tokio::test]
1518 async fn stream_text_oversized_attributes_errors() {
1519 let (m, sent) = setup();
1520 let mut opts = text_opts("chat", &[]);
1521 opts.attributes.insert("big".to_string(), "x".repeat(20_000));
1522 let result = m.stream_text(opts).await;
1523 assert!(matches!(result, Err(StreamError::HeaderTooLarge)));
1524 assert!(sent.lock().unwrap().is_empty());
1526 }
1527
1528 #[tokio::test]
1529 async fn stream_text_splits_on_utf8_boundaries_at_mtu() {
1530 let (m, sent) = setup();
1531 let text = format!("{}😀{}", "a".repeat(14_999), "b".repeat(10));
1533 let writer = m.stream_text(text_opts("chat", &[])).await.unwrap();
1534 writer.write(&text).await.unwrap();
1535 writer.close().await.unwrap();
1536
1537 let p = sent.lock().unwrap().clone();
1538 assert_eq!(p.len(), 4); let first = chunk(&p[1]);
1540 let second = chunk(&p[2]);
1541 assert_eq!(first.content.len(), 14_999);
1544 let first_str =
1545 std::str::from_utf8(&first.content).expect("chunk 0 must be valid UTF-8");
1546 let second_str =
1547 std::str::from_utf8(&second.content).expect("chunk 1 must be valid UTF-8");
1548 assert_eq!(format!("{first_str}{second_str}"), text);
1549 }
1550 }
1551}