yawc 0.3.3

Yet another websocket library. But a fast, secure WebSocket implementation with RFC 6455 compliance and compression support
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
//! WebSocket connection configuration options.

use std::time::Duration;

use crate::compression::WebSocketExtensions;

/// Type alias for the compression level used in WebSocket compression settings.
///
/// This alias refers to `flate2::Compression`, allowing easy configuration of compression levels
/// when creating compressors and decompressors for WebSocket frames.
pub type CompressionLevel = flate2::Compression;

/// Configuration options for a WebSocket connection.
///
/// `Options` allows comprehensive control over WebSocket connection behavior, including:
/// - **Payload size limits**: Control memory usage and prevent abuse
/// - **Compression**: Reduce bandwidth using permessage-deflate (RFC 7692)
/// - **Fragmentation**: Handle large messages
/// - **UTF-8 validation**: Ensure text frame compliance
/// - **TCP options**: Fine-tune network behavior
///
/// # Protocol Layers
///
/// The WebSocket implementation is organized into distinct processing layers:
///
/// ## 1. Fragmentation Layer
/// Handles splitting and reassembling messages across multiple frames. Controlled by
/// [`Options::fragmentation`] (see [`Fragmentation`] for details).
///
/// - **Outgoing**: Automatically fragments large messages when [`Fragmentation::fragment_size`] is set
/// - **Incoming**: Automatically reassembles fragments into complete messages before delivery
///
/// ## 2. Compression Layer
/// Applies permessage-deflate compression to reduce bandwidth. Controlled by
/// [`Options::compression`] (see [`DeflateOptions`] for details).
///
/// - Operates on complete messages (after fragmentation/before defragmentation)
/// - Can be configured with different compression levels and context takeover modes
/// - Negotiated during the WebSocket handshake
///
/// # Common Patterns
///
/// ## Basic Configuration
/// ```rust
/// use yawc::Options;
///
/// let options = Options::default()
///     .with_max_payload_read(1024 * 1024)  // 1 MiB max incoming
///     .with_utf8();                         // Validate text frames
/// ```
///
/// ## CPU and Memory-Constrained Environment
/// ```rust
/// use yawc::Options;
///
/// let options = Options::default()
///     .with_limits(128 * 1024, 256 * 1024)  // Small payload/buffer limits
///     .without_compression();                 // Avoid compression overhead
/// ```
#[derive(Clone, Default)]
pub struct Options {
    /// Maximum allowed payload size for incoming messages, in bytes.
    ///
    /// If a message exceeds this size, the connection will be closed immediately
    /// to prevent overloading the receiving end.
    ///
    /// Default: 1 MiB (1,048,576 bytes) as defined in [`super::MAX_PAYLOAD_READ`]
    pub max_payload_read: Option<usize>,

    /// Maximum size allowed for the buffer that accumulates fragmented messages.
    ///
    /// WebSocket messages can be split into multiple fragments for transmission. These fragments
    /// are accumulated in a read buffer until the complete message is received. To prevent memory
    /// exhaustion attacks using extremely large fragmented messages, once the total size of
    /// accumulated fragments exceeds this limit, the connection is closed.
    ///
    /// Default: 2 MiB (2,097,152 bytes) as defined in [`super::MAX_READ_BUFFER`], or twice the
    /// configured `max_payload_read` value if that is set.
    pub max_read_buffer: Option<usize>,

    /// Compression settings for the WebSocket connection.
    ///
    /// Compression is based on the Deflate algorithm, with additional configuration available
    /// through [`DeflateOptions`] for finer control over the compression strategy.
    pub compression: Option<DeflateOptions>,

    /// Configuration for message fragmentation behavior.
    ///
    /// Controls how the WebSocket handles fragmented messages, including whether to
    /// reassemble fragments, timeouts for receiving fragments, and automatic fragmentation
    /// of outgoing messages.
    ///
    /// See [`Fragmentation`] for available options.
    pub fragmentation: Option<Fragmentation>,

    /// Flag to determine whether incoming messages should be validated for UTF-8 encoding.
    ///
    /// If `true`, the [`super::ReadHalf`] will validate that received text frames contain valid UTF-8
    /// data, closing the connection on any validation failure.
    ///
    /// Default: `false`
    pub check_utf8: bool,

    /// Flag to disable [Nagle's Algorithm](https://en.wikipedia.org/wiki/Nagle%27s_algorithm) on the TCP socket.
    ///
    /// If `true`, outgoing WebSocket messages will be sent immediately without waiting
    /// for additional data to be buffered, potentially improving latency.
    ///
    /// Default: `false`
    pub no_delay: bool,

    /// Backpressure boundary for the write buffer in bytes.
    ///
    /// When the write buffer exceeds this size, backpressure is applied to
    /// prevent unbounded memory growth. This helps with flow control when
    /// sending large amounts of data.
    ///
    /// Default: `None` (uses tokio-util default)
    pub max_backpressure_write_boundary: Option<usize>,
}

/// Configuration for WebSocket message fragmentation.
///
/// The WebSocket protocol allows large messages to be split into multiple fragments
/// for transmission. This struct controls how both incoming and outgoing fragmented
/// messages are handled.
///
/// # Fragmentation Modes
///
/// ## Standard Mode (default)
/// By default, incoming fragments are automatically reassembled into complete messages
/// before being returned to the application. This provides a simple interface where
/// applications always receive complete messages.
///
/// # Automatic Fragmentation
/// For outgoing messages, the `fragment_size` option enables automatic fragmentation
/// of large messages. This is useful for:
/// - Preventing large messages from blocking the send queue
/// - Ensuring fair interleaving of multiple concurrent messages
/// - Working with size constraints imposed by intermediaries
///
/// # Fragment Timeout
/// The `timeout` option protects against incomplete messages by setting a maximum
/// duration to wait for all fragments to arrive.
///
/// # Example
/// ```
/// use std::time::Duration;
/// use yawc::Options;
///
/// let options = Options::default()
///     .with_fragment_timeout(Duration::from_secs(30))
///     .with_max_fragment_size(64 * 1024); // 64 KiB fragments
/// ```
#[derive(Clone, Default, Debug)]
pub struct Fragmentation {
    /// Maximum time allowed to receive all fragments of a fragmented message.
    ///
    /// When receiving a fragmented WebSocket message, this timeout limits how long
    /// the connection will wait for all fragments to arrive. If the timeout expires
    /// before the final fragment is received, the connection returns a
    /// [`FragmentTimeout`](crate::WebSocketError::FragmentTimeout) error.
    ///
    /// This protects against slow-loris style attacks where a peer sends fragments
    /// very slowly to hold resources.
    ///
    /// Default: `None` (no timeout)
    pub timeout: Option<Duration>,

    /// Maximum size for each fragment when automatically splitting outgoing messages.
    ///
    /// When set, outgoing messages that exceed this size will be automatically
    /// fragmented into multiple frames. Each fragment will have a payload size
    /// at or below this limit.
    ///
    /// This is useful for:
    /// - Preventing large messages from blocking smaller ones
    /// - Working with intermediaries that have message size limits
    /// - Controlling memory usage in the send buffer
    ///
    /// Default: `None` (no automatic fragmentation)
    pub fragment_size: Option<usize>,
}

impl Options {
    /// Configures payload and buffer size limits.
    ///
    /// This is a convenience method to set both limits at once instead of calling
    /// `with_max_payload_read()` and `with_max_read_buffer()` separately.
    ///
    /// # Parameters
    /// - `max_payload`: Maximum size for a single frame's payload
    /// - `max_buffer`: Maximum size for accumulated fragmented message data
    ///
    /// # Example
    /// ```rust
    /// use yawc::Options;
    ///
    /// let options = Options::default()
    ///     .with_limits(2 * 1024 * 1024, 4 * 1024 * 1024); // 2MB payload, 4MB buffer
    /// ```
    pub fn with_limits(self, max_payload: usize, max_buffer: usize) -> Self {
        Self {
            max_payload_read: Some(max_payload),
            max_read_buffer: Some(max_buffer),
            ..self
        }
    }

    /// Configures compression using a preset profile optimized for low latency.
    ///
    /// This is equivalent to using `DeflateOptions::low_latency()`.
    /// Use this for real-time applications where response time is critical.
    ///
    /// # Example
    /// ```rust
    /// use yawc::Options;
    ///
    /// let options = Options::default().with_low_latency_compression();
    /// ```
    pub fn with_low_latency_compression(self) -> Self {
        Self {
            compression: Some(DeflateOptions::low_latency()),
            ..self
        }
    }

    /// Configures compression using a preset profile optimized for maximum compression ratio.
    ///
    /// This is equivalent to using `DeflateOptions::high_compression()`.
    /// Use this when bandwidth is limited and CPU resources are available.
    ///
    /// # Example
    /// ```rust
    /// use yawc::Options;
    ///
    /// let options = Options::default().with_high_compression();
    /// ```
    pub fn with_high_compression(self) -> Self {
        Self {
            compression: Some(DeflateOptions::high_compression()),
            ..self
        }
    }

    /// Configures compression using a balanced preset profile.
    ///
    /// This is equivalent to using `DeflateOptions::balanced()`.
    /// This is a good default for most applications.
    ///
    /// # Example
    /// ```rust
    /// use yawc::Options;
    ///
    /// let options = Options::default().with_balanced_compression();
    /// ```
    pub fn with_balanced_compression(self) -> Self {
        Self {
            compression: Some(DeflateOptions::balanced()),
            ..self
        }
    }

    /// Sets the compression level for outgoing messages.
    ///
    /// Adjusts the compression level used for message transmission, allowing a balance between
    /// compression efficiency and CPU usage. This option is useful for controlling bandwidth
    /// while managing resource consumption.
    ///
    /// The WebSocket protocol supports two main compression approaches:
    /// - Per-message compression (RFC 7692) using the permessage-deflate extension
    /// - Context takeover, which maintains compression state between messages
    ///
    /// Compression levels range from 0-9:
    /// - 0: No compression (fastest)
    /// - 1-3: Low compression, minimal CPU usage
    /// - 4-6: Balanced compression/CPU trade-off
    /// - 7-9: Maximum compression, highest CPU usage
    ///
    /// For real-time applications with latency constraints, lower compression levels (1-3)
    /// are recommended. For bandwidth-constrained scenarios where CPU usage is less critical,
    /// higher levels (7-9) may be preferable.
    ///
    /// # Parameters
    /// - `level`: The desired compression level, based on the [`CompressionLevel`] type.
    ///
    /// # Returns
    /// A modified `Options` instance with the specified compression level.
    ///
    /// # Example
    /// ```rust
    /// use yawc::{Options, CompressionLevel};
    ///
    /// let options = Options::default()
    ///     .with_compression_level(CompressionLevel::new(6)) // Balanced compression
    ///     .with_utf8(); // Enable UTF-8 validation
    /// ```
    pub fn with_compression_level(self, level: CompressionLevel) -> Self {
        let mut compression = self.compression.unwrap_or_default();
        compression.level = level;

        Self {
            compression: Some(compression),
            ..self
        }
    }

    /// Disables compression for the WebSocket connection.
    ///
    /// Removes any previously configured compression settings, ensuring that
    /// messages sent over this connection will not be compressed. This is useful
    /// when compression would add unnecessary overhead, such as when sending
    /// already-compressed data or small messages.
    ///
    /// # Returns
    /// A modified `Options` instance with compression disabled.
    pub fn without_compression(self) -> Self {
        Self {
            compression: None,
            ..self
        }
    }

    /// Sets the maximum allowed payload size for incoming messages.
    ///
    /// Specifies the maximum size of messages that the WebSocket connection will accept.
    /// If an incoming message exceeds this size, the connection will be terminated to avoid
    /// overloading the receiver.
    ///
    /// # Parameters
    /// - `size`: The maximum payload size in bytes.
    ///
    /// # Returns
    /// A modified `Options` instance with the specified payload size limit.
    pub fn with_max_payload_read(self, size: usize) -> Self {
        Self {
            max_payload_read: Some(size),
            ..self
        }
    }

    /// Sets the maximum read buffer size for accumulated fragmented messages.
    ///
    /// When receiving fragmented WebSocket messages, data is accumulated in a read buffer.
    /// Once this buffer exceeds the specified size limit, it will be reset back to the
    /// initial 8 KiB capacity to prevent unbounded memory growth from very large fragmented
    /// messages.
    ///
    /// # Parameters
    /// - `size`: Maximum size in bytes allowed for the read buffer
    ///
    /// # Returns
    /// A modified `Options` instance with the specified read buffer size limit.
    pub fn with_max_read_buffer(self, size: usize) -> Self {
        Self {
            max_read_buffer: Some(size),
            ..self
        }
    }

    /// Enables UTF-8 validation for incoming text messages.
    ///
    /// When enabled, the WebSocket connection will verify that all received text messages
    /// contain valid UTF-8 data. This is particularly useful for cases where the source of messages
    /// is untrusted, ensuring data integrity.
    ///
    /// # Returns
    /// A modified `Options` instance with UTF-8 validation enabled.
    pub fn with_utf8(self) -> Self {
        Self {
            check_utf8: true,
            ..self
        }
    }

    /// Enables TCP_NODELAY on the TCP stream.
    ///
    /// When enabled, [Nagle's Algorithm](https://en.wikipedia.org/wiki/Nagle%27s_algorithm) will be
    /// disabled on the TCP socket. WebSocket messages will be sent immediately without waiting
    /// for additional data to be buffered, potentially improving latency.
    ///
    /// # Returns
    /// A modified `Options` instance with TCP_NODELAY enabled.
    pub fn with_no_delay(self) -> Self {
        Self {
            no_delay: true,
            ..self
        }
    }

    /// Sets the maximum time allowed to receive all fragments of a fragmented message.
    ///
    /// This protects against slow-loris style attacks where a peer sends fragments
    /// very slowly to hold connection resources.
    ///
    /// # Parameters
    /// - `timeout`: Maximum duration to wait for all fragments
    ///
    /// # Returns
    /// A modified `Options` instance with the specified fragment timeout.
    ///
    /// # Example
    /// ```rust
    /// use std::time::Duration;
    /// use yawc::Options;
    ///
    /// let options = Options::default()
    ///     .with_fragment_timeout(Duration::from_secs(30));
    /// ```
    pub fn with_fragment_timeout(self, timeout: Duration) -> Self {
        let mut fragmentation = self.fragmentation.unwrap_or_default();
        fragmentation.timeout = Some(timeout);
        Self {
            fragmentation: Some(fragmentation),
            ..self
        }
    }

    /// Sets the maximum fragment size for automatic fragmentation of outgoing messages.
    ///
    /// When set, outgoing messages that exceed this size will be automatically
    /// fragmented into multiple frames. Each fragment will have a payload size
    /// at or below this limit.
    ///
    /// # Parameters
    /// - `size`: Maximum fragment size in bytes
    ///
    /// # Returns
    /// A modified `Options` instance with the specified fragment size.
    ///
    /// # Example
    /// ```rust
    /// use yawc::Options;
    ///
    /// let options = Options::default()
    ///     .with_max_fragment_size(64 * 1024); // 64 KiB max per fragment
    /// ```
    pub fn with_max_fragment_size(self, size: usize) -> Self {
        let mut fragmentation = self.fragmentation.unwrap_or_default();
        fragmentation.fragment_size = Some(size);
        Self {
            fragmentation: Some(fragmentation),
            ..self
        }
    }

    /// Sets the backpressure boundary for the write buffer.
    ///
    /// When the write buffer exceeds this size, backpressure is applied to
    /// prevent unbounded memory growth. This is useful for flow control when
    /// sending large amounts of data.
    ///
    /// # Parameters
    /// - `size`: Backpressure boundary in bytes
    ///
    /// # Returns
    /// A modified `Options` instance with the specified backpressure boundary.
    ///
    /// # Example
    /// ```rust
    /// use yawc::Options;
    ///
    /// let options = Options::default()
    ///     .with_backpressure_boundary(128 * 1024); // 128 KiB boundary
    /// ```
    pub fn with_backpressure_boundary(self, size: usize) -> Self {
        Self {
            max_backpressure_write_boundary: Some(size),
            ..self
        }
    }

    /// Sets the maximum window size for the client's decompression (LZ77) window.
    ///
    /// Limits the size of the client's decompression window, used during message decompression.
    /// This option is available only when compiled with the `zlib` feature.
    ///
    /// # Parameters
    /// - `max_window_bits`: The maximum number of bits for the client decompression window.
    ///
    /// # Returns
    /// A modified `Options` instance with the specified client max window size.
    #[cfg(feature = "zlib")]
    pub fn with_client_max_window_bits(self, max_window_bits: u8) -> Self {
        let mut compression = self.compression.unwrap_or_default();
        compression.client_max_window_bits = Some(max_window_bits);
        Self {
            compression: Some(compression),
            ..self
        }
    }

    /// Configures the maximum window size for server-side compression.
    ///
    /// The window size determines how much historical data the compressor can reference
    /// when encoding new data. Larger windows generally provide better compression but
    /// require more memory.
    ///
    /// Available only with the `zlib` feature enabled.
    ///
    /// # Parameters
    /// - `max_window_bits`: The maximum number of bits for the server compression window (9-15).
    ///
    /// # Returns
    /// A modified `Options` instance with the specified server max window size.
    #[cfg(feature = "zlib")]
    pub fn with_server_max_window_bits(self, max_window_bits: u8) -> Self {
        let mut compression = self.compression.unwrap_or_default();
        compression.server_max_window_bits = Some(max_window_bits);
        Self {
            compression: Some(compression),
            ..self
        }
    }

    /// Disables context takeover for server-side compression.
    ///
    /// In the WebSocket permessage-deflate extension, "context takeover" refers to maintaining
    /// the compression dictionary between messages. With context takeover enabled (default),
    /// the compression algorithm builds and reuses a dictionary of repeated patterns across
    /// multiple messages, potentially achieving better compression ratios for similar data.
    ///
    /// When disabled via this option:
    /// - The compression dictionary is reset after each message
    /// - Memory usage remains constant since the dictionary isn't preserved
    /// - Compression ratio may be lower since patterns can't be reused
    /// - Particularly useful for long-lived connections where memory growth is a concern
    ///
    /// This setting corresponds to the "server_no_context_takeover" extension parameter
    /// in the WebSocket protocol negotiation.
    ///
    /// # Returns
    /// A modified `Options` instance with server context takeover disabled.
    pub fn server_no_context_takeover(self) -> Self {
        let mut compression = self.compression.unwrap_or_default();
        compression.server_no_context_takeover = true;
        Self {
            compression: Some(compression),
            ..self
        }
    }

    /// Disables context takeover for client-side compression.
    ///
    /// In the WebSocket permessage-deflate extension, "context takeover" refers to maintaining
    /// the compression dictionary between messages. The client's compression context is separate
    /// from the server's, allowing asymmetric configuration based on each endpoint's capabilities.
    ///
    /// When disabled via this option:
    /// - The client's compression dictionary is reset after each message
    /// - Client memory usage remains constant since the dictionary isn't preserved
    /// - May reduce compression efficiency but prevents memory growth on clients
    /// - Useful for memory-constrained clients like mobile devices or browsers
    ///
    /// This setting corresponds to the "client_no_context_takeover" extension parameter
    /// in the WebSocket protocol negotiation.
    ///
    /// # Returns
    /// A modified `Options` instance with client context takeover disabled.
    pub fn client_no_context_takeover(self) -> Self {
        let mut compression = self.compression.unwrap_or_default();
        compression.client_no_context_takeover = true;
        Self {
            compression: Some(compression),
            ..self
        }
    }
}

/// Configuration options for WebSocket message compression using the Deflate algorithm.
///
/// The WebSocket protocol supports per-message compression using a Deflate-based algorithm
/// (RFC 7692) to reduce bandwidth usage. This struct allows fine-grained control over
/// compression behavior and memory usage through several key settings:
///
/// # Compression Level
/// Controls the tradeoff between compression ratio and CPU usage via the `level` field.
/// Higher levels provide better compression but require more processing time.
///
/// # Context Management
/// Offers two modes for managing the compression context:
///
/// - **Context Takeover** (default): Maintains compression state between messages,
///   providing better compression ratios at the cost of increased memory usage.
///   Ideal for applications prioritizing bandwidth efficiency.
///
/// - **No Context Takeover**: Resets compression state after each message,
///   reducing memory usage at the expense of compression efficiency.
///   Better suited for memory-constrained environments.
///
/// # Memory Window Size
/// When the `zlib` feature is enabled, allows precise control over the compression
/// window size for both client and server, enabling further optimization of the
/// memory-compression tradeoff.
///
/// # Example
/// ```
/// use yawc::{DeflateOptions, CompressionLevel};
///
/// let opts = DeflateOptions {
///     level: CompressionLevel::default(),
///     server_no_context_takeover: true,
///     ..Default::default()
/// };
/// ```
#[derive(Clone, Default)]
pub struct DeflateOptions {
    /// Sets the compression level (0-9), balancing compression ratio against CPU usage.
    ///
    /// - 0: No compression (fastest)
    /// - 1-3: Low compression (fast)
    /// - 4-6: Medium compression (default)
    /// - 7-9: High compression (slow)
    pub level: CompressionLevel,

    /// Controls the compression window size (in bits) for server-side compression.
    ///
    /// Larger windows improve compression but use more memory. Available only with
    /// the `zlib` feature enabled. Valid range: 8-15 bits.
    #[cfg(feature = "zlib")]
    pub server_max_window_bits: Option<u8>,

    /// Controls the compression window size (in bits) for client-side compression.
    ///
    /// Larger windows improve compression but use more memory. Available only with
    /// the `zlib` feature enabled. Valid range: 8-15 bits.
    #[cfg(feature = "zlib")]
    pub client_max_window_bits: Option<u8>,

    /// Controls server-side compression context management.
    ///
    /// When `true`, compression state is reset after each message, reducing
    /// memory usage at the cost of compression efficiency.
    pub server_no_context_takeover: bool,

    /// Controls client-side compression context management.
    ///
    /// When `true`, compression state is reset after each message, reducing
    /// memory usage at the cost of compression efficiency.
    pub client_no_context_takeover: bool,
}

impl DeflateOptions {
    /// Creates compression settings optimized for low latency.
    ///
    /// This profile uses:
    /// - Fast compression level (level 1)
    /// - No context takeover on both sides to minimize memory and processing
    ///
    /// Best for real-time applications where latency is critical.
    pub fn low_latency() -> Self {
        Self {
            level: CompressionLevel::fast(),
            #[cfg(feature = "zlib")]
            server_max_window_bits: None,
            #[cfg(feature = "zlib")]
            client_max_window_bits: None,
            server_no_context_takeover: false,
            client_no_context_takeover: false,
        }
    }

    /// Creates compression settings optimized for maximum compression.
    ///
    /// This profile uses:
    /// - Best compression level (level 9)
    /// - Context takeover enabled for better compression ratios
    ///
    /// Best for bandwidth-constrained scenarios where CPU is available.
    pub fn high_compression() -> Self {
        Self {
            level: CompressionLevel::best(),
            #[cfg(feature = "zlib")]
            server_max_window_bits: None,
            #[cfg(feature = "zlib")]
            client_max_window_bits: None,
            server_no_context_takeover: false,
            client_no_context_takeover: false,
        }
    }

    /// Creates balanced compression settings.
    ///
    /// This profile uses:
    /// - Default compression level (level 6)
    /// - Moderate window sizes
    /// - No context takeover to prevent memory growth
    ///
    /// Best general-purpose configuration for most applications.
    pub fn balanced() -> Self {
        Self {
            level: CompressionLevel::default(),
            #[cfg(feature = "zlib")]
            server_max_window_bits: None,
            #[cfg(feature = "zlib")]
            client_max_window_bits: None,
            server_no_context_takeover: false,
            client_no_context_takeover: false,
        }
    }

    /// Called by the server when upgrading.
    pub(super) fn merge(&self, offered: &WebSocketExtensions) -> WebSocketExtensions {
        WebSocketExtensions {
            // Accept client's no_context_takeover settings
            client_no_context_takeover: offered.client_no_context_takeover
                || self.client_no_context_takeover,
            server_no_context_takeover: offered.server_no_context_takeover
                || self.server_no_context_takeover,
            // For window bits, take the minimum of what client offers and what server supports
            #[cfg(feature = "zlib")]
            client_max_window_bits: match (
                offered.client_max_window_bits,
                self.client_max_window_bits,
            ) {
                // Client offers specific value, server has preference: take minimum
                (Some(Some(c)), Some(s)) => Some(Some(c.min(s))),
                // Client offers specific value, server has no preference: use client's
                (Some(Some(c)), None) => Some(Some(c)),
                // Client offers parameter without value, server has preference: use server's
                (Some(None), Some(s)) => Some(Some(s)),
                // Client offers parameter without value, server has no preference: use the minimum value
                (Some(None), None) => Some(Some(9)),
                // Client doesn't offer, use server's preference (if any)
                (None, s) => s.map(Some),
            },
            #[cfg(feature = "zlib")]
            server_max_window_bits: match (
                offered.server_max_window_bits,
                self.server_max_window_bits,
            ) {
                // Client offers specific value, server has preference: take minimum
                (Some(Some(c)), Some(s)) => Some(Some(c.min(s))),
                // Client offers specific value, server has no preference: use client's
                (Some(Some(c)), None) => Some(Some(c)),
                // Client offers parameter without value, server has preference: use server's
                (Some(None), Some(s)) => Some(Some(s)),
                // Client offers parameter without value, server has no preference: use the minimum value
                (Some(None), None) => Some(Some(9)),
                // Client doesn't offer, use server's preference (if any)
                (None, s) => s.map(Some),
            },
            #[cfg(not(feature = "zlib"))]
            client_max_window_bits: None,
            #[cfg(not(feature = "zlib"))]
            server_max_window_bits: None,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_merge_no_context_takeover() {
        let server = DeflateOptions {
            level: CompressionLevel::default(),
            #[cfg(feature = "zlib")]
            server_max_window_bits: None,
            #[cfg(feature = "zlib")]
            client_max_window_bits: None,
            server_no_context_takeover: false,
            client_no_context_takeover: false,
        };

        let client_offer = WebSocketExtensions {
            server_max_window_bits: None,
            client_max_window_bits: None,
            server_no_context_takeover: true,
            client_no_context_takeover: true,
        };

        let merged = server.merge(&client_offer);

        assert!(merged.server_no_context_takeover);
        assert!(merged.client_no_context_takeover);
    }

    #[test]
    fn test_merge_no_context_takeover_server_requires() {
        let server = DeflateOptions {
            level: CompressionLevel::default(),
            #[cfg(feature = "zlib")]
            server_max_window_bits: None,
            #[cfg(feature = "zlib")]
            client_max_window_bits: None,
            server_no_context_takeover: true,
            client_no_context_takeover: false,
        };

        let client_offer = WebSocketExtensions {
            server_max_window_bits: None,
            client_max_window_bits: None,
            server_no_context_takeover: false,
            client_no_context_takeover: false,
        };

        let merged = server.merge(&client_offer);

        // Server requires no_context_takeover, so it's enabled even if client doesn't request it
        assert!(merged.server_no_context_takeover);
        assert!(!merged.client_no_context_takeover);
    }

    #[cfg(feature = "zlib")]
    #[test]
    fn test_merge_window_bits_takes_minimum() {
        let server = DeflateOptions {
            level: CompressionLevel::default(),
            server_max_window_bits: Some(15),
            client_max_window_bits: Some(15),
            server_no_context_takeover: false,
            client_no_context_takeover: false,
        };

        let client_offer = WebSocketExtensions {
            server_max_window_bits: Some(Some(12)),
            client_max_window_bits: Some(Some(10)),
            server_no_context_takeover: false,
            client_no_context_takeover: false,
        };

        let merged = server.merge(&client_offer);

        // Should take the minimum of client and server values
        assert_eq!(merged.server_max_window_bits, Some(Some(12)));
        assert_eq!(merged.client_max_window_bits, Some(Some(10)));
    }

    #[cfg(feature = "zlib")]
    #[test]
    fn test_merge_window_bits_client_only() {
        let server = DeflateOptions {
            level: CompressionLevel::default(),
            #[cfg(feature = "zlib")]
            server_max_window_bits: None,
            #[cfg(feature = "zlib")]
            client_max_window_bits: None,
            server_no_context_takeover: false,
            client_no_context_takeover: false,
        };

        let client_offer = WebSocketExtensions {
            server_max_window_bits: Some(Some(12)),
            client_max_window_bits: Some(Some(10)),
            server_no_context_takeover: false,
            client_no_context_takeover: false,
        };

        let merged = server.merge(&client_offer);

        // Server accepts client's offered values when server has no preference
        assert_eq!(merged.server_max_window_bits, Some(Some(12)));
        assert_eq!(merged.client_max_window_bits, Some(Some(10)));
    }

    #[cfg(feature = "zlib")]
    #[test]
    fn test_merge_window_bits_server_only() {
        let server = DeflateOptions {
            level: CompressionLevel::default(),
            #[cfg(feature = "zlib")]
            server_max_window_bits: Some(14),
            #[cfg(feature = "zlib")]
            client_max_window_bits: Some(13),
            server_no_context_takeover: false,
            client_no_context_takeover: false,
        };

        let client_offer = WebSocketExtensions {
            server_max_window_bits: None,
            client_max_window_bits: None,
            server_no_context_takeover: false,
            client_no_context_takeover: false,
        };

        let merged = server.merge(&client_offer);

        // When client doesn't specify, use server's preference
        assert_eq!(merged.server_max_window_bits, Some(Some(14)));
        assert_eq!(merged.client_max_window_bits, Some(Some(13)));
    }

    #[cfg(feature = "zlib")]
    #[test]
    fn test_merge_window_bits_none_both() {
        let server = DeflateOptions {
            level: CompressionLevel::default(),
            #[cfg(feature = "zlib")]
            server_max_window_bits: None,
            #[cfg(feature = "zlib")]
            client_max_window_bits: None,
            server_no_context_takeover: false,
            client_no_context_takeover: false,
        };

        let client_offer = WebSocketExtensions {
            server_max_window_bits: None,
            client_max_window_bits: None,
            server_no_context_takeover: false,
            client_no_context_takeover: false,
        };

        let merged = server.merge(&client_offer);

        // When neither specifies, should be None
        assert_eq!(merged.server_max_window_bits, None);
        assert_eq!(merged.client_max_window_bits, None);
    }

    #[test]
    fn test_merge_mixed_options() {
        #[cfg(feature = "zlib")]
        let server = DeflateOptions {
            level: CompressionLevel::default(),
            server_max_window_bits: Some(15),
            client_max_window_bits: Some(14),
            server_no_context_takeover: true,
            client_no_context_takeover: false,
        };

        #[cfg(not(feature = "zlib"))]
        let server = DeflateOptions {
            level: CompressionLevel::default(),
            server_no_context_takeover: true,
            client_no_context_takeover: false,
        };

        #[cfg(feature = "zlib")]
        let client_offer = WebSocketExtensions {
            server_max_window_bits: Some(Some(12)),
            client_max_window_bits: Some(Some(13)),
            server_no_context_takeover: false,
            client_no_context_takeover: true,
        };

        #[cfg(not(feature = "zlib"))]
        let client_offer = WebSocketExtensions {
            server_max_window_bits: None,
            client_max_window_bits: None,
            server_no_context_takeover: false,
            client_no_context_takeover: true,
        };

        let merged = server.merge(&client_offer);

        // Both server and client request no_context_takeover for their respective sides
        assert!(merged.server_no_context_takeover);
        assert!(merged.client_no_context_takeover);

        #[cfg(feature = "zlib")]
        {
            // Window bits should take minimum
            assert_eq!(merged.server_max_window_bits, Some(Some(12)));
            assert_eq!(merged.client_max_window_bits, Some(Some(13)));
        }

        #[cfg(not(feature = "zlib"))]
        {
            // Without zlib feature, window bits should be None
            assert_eq!(merged.server_max_window_bits, None);
            assert_eq!(merged.client_max_window_bits, None);
        }
    }

    #[cfg(feature = "zlib")]
    #[test]
    fn test_merge_client_offers_no_value() {
        let server = DeflateOptions {
            level: CompressionLevel::default(),
            server_max_window_bits: Some(14),
            client_max_window_bits: Some(13),
            server_no_context_takeover: false,
            client_no_context_takeover: false,
        };

        let client_offer = WebSocketExtensions {
            server_max_window_bits: Some(None), // Client offers parameter without value
            client_max_window_bits: Some(None), // Client offers parameter without value
            server_no_context_takeover: false,
            client_no_context_takeover: false,
        };

        let merged = server.merge(&client_offer);

        // When client offers without value, use server's preference
        assert_eq!(merged.server_max_window_bits, Some(Some(14)));
        assert_eq!(merged.client_max_window_bits, Some(Some(13)));
    }

    #[cfg(feature = "zlib")]
    #[test]
    fn test_merge_client_offers_no_value_server_no_preference() {
        let server = DeflateOptions {
            level: CompressionLevel::default(),
            server_max_window_bits: None,
            client_max_window_bits: None,
            server_no_context_takeover: false,
            client_no_context_takeover: false,
        };

        let client_offer = WebSocketExtensions {
            server_max_window_bits: Some(None), // Client offers parameter without value
            client_max_window_bits: Some(None), // Client offers parameter without value
            server_no_context_takeover: false,
            client_no_context_takeover: false,
        };

        let merged = server.merge(&client_offer);

        // When client offers without value and server has no preference, leave unspecified
        assert_eq!(merged.server_max_window_bits, Some(Some(9)));
        assert_eq!(merged.client_max_window_bits, Some(Some(9)));
    }

    #[test]
    fn test_parse_and_merge_client_offers_client_max_window_bits_no_value() {
        use std::str::FromStr;

        // Parse client offer: "permessage-deflate; client_max_window_bits"
        let client_offer =
            WebSocketExtensions::from_str("permessage-deflate; client_max_window_bits").unwrap();

        // Verify client offered the parameter without a value
        assert_eq!(client_offer.client_max_window_bits, Some(None));
        assert_eq!(client_offer.server_max_window_bits, None);

        #[cfg(feature = "zlib")]
        {
            // Server has a preference for client_max_window_bits
            let server = DeflateOptions {
                level: CompressionLevel::default(),
                server_max_window_bits: Some(15),
                client_max_window_bits: Some(12), // Server prefers 12
                server_no_context_takeover: false,
                client_no_context_takeover: false,
            };

            let merged = server.merge(&client_offer);

            // Server should respond with its preference
            assert_eq!(merged.client_max_window_bits, Some(Some(12)));
            assert_eq!(merged.server_max_window_bits, Some(Some(15)));

            // Verify the response string includes the value
            let response = merged.to_string();
            assert!(response.contains("client_max_window_bits=12"));
            assert!(response.contains("server_max_window_bits=15"));
        }

        #[cfg(not(feature = "zlib"))]
        {
            // Without zlib, server can't negotiate window bits
            let server = DeflateOptions {
                level: CompressionLevel::default(),
                server_no_context_takeover: false,
                client_no_context_takeover: false,
            };

            let merged = server.merge(&client_offer);

            // Should be None without zlib feature
            assert_eq!(merged.client_max_window_bits, None);
            assert_eq!(merged.server_max_window_bits, None);
        }
    }
}