aggligator_wrapper_zstd/lib.rs
1#![warn(missing_docs)]
2#![cfg_attr(docsrs, feature(doc_cfg))]
3#![doc(
4 html_logo_url = "https://raw.githubusercontent.com/remoc-rs/aggligator/master/.misc/aggligator.png",
5 html_favicon_url = "https://raw.githubusercontent.com/remoc-rs/aggligator/master/.misc/aggligator.png",
6 issue_tracker_base_url = "https://github.com/remoc-rs/aggligator/issues/"
7)]
8
9//! [Aggligator](aggligator) transport wrapper using zstd compression
10//!
11//! This compresses the data sent over each Aggligator link using [zstd](https://facebook.github.io/zstd/).
12//!
13//! The wrapper must be used on both ends of a connection, since compression is not negotiated.
14//! The compression level only affects the sending side and thus may differ between both ends.
15//!
16//! Compression only pays off when the transferred data is compressible and the links,
17//! not the CPU, are the bottleneck. Data that is already compressed or encrypted does not
18//! become smaller. Alternatively, the application can compress the data of the aggregated
19//! connection itself, which compresses the data only once instead of separately on each link.
20//!
21//! # Combining with TLS
22//! Wrappers are applied in the order they are added, thus the first wrapper is applied directly
23//! to the transport. To compress data before it is encrypted, add the TLS wrapper first and then
24//! this wrapper.
25//!
26//! Compressing data before encrypting it may reveal information about the data through
27//! the length of the encrypted data. This allows attacks if data controlled by an attacker is
28//! transferred together with secret data over the same connection.
29//!
30//! # Example
31//! ```no_run
32//! use aggligator::transport::ConnectorBuilder;
33//! use aggligator_wrapper_zstd::Zstd;
34//!
35//! let mut builder = ConnectorBuilder::new(Default::default());
36//! builder.wrap(Zstd::new());
37//! let connector = builder.build();
38//! ```
39
40use async_compression::{
41 Level,
42 tokio::{bufread::ZstdDecoder, write::ZstdEncoder},
43 zstd::{CParameter, DParameter},
44};
45use async_trait::async_trait;
46use std::io::{Error, ErrorKind, Result};
47use tokio::io::BufReader;
48
49use aggligator::{
50 io::{IoBox, StreamBox},
51 transport::{AcceptingWrapper, ConnectingWrapper},
52};
53
54static NAME: &str = "zstd";
55
56/// zstd compression wrapper for outgoing and incoming links.
57///
58/// Only IO-based streams are supported; wrapping a packet-based stream fails.
59///
60/// Add this to a [`ConnectorBuilder`](aggligator::transport::ConnectorBuilder) and the
61/// [`AcceptorBuilder`](aggligator::transport::AcceptorBuilder) of the remote endpoint
62/// to compress all links of the connection.
63#[derive(Debug, Clone)]
64#[must_use = "you must pass this wrapper to the connector or acceptor"]
65pub struct Zstd {
66 level: i32,
67 window_log: Option<u32>,
68}
69
70impl Default for Zstd {
71 fn default() -> Self {
72 Self::new()
73 }
74}
75
76impl Zstd {
77 /// Default compression level.
78 ///
79 /// This is the fastest regular level of zstd.
80 pub const DEFAULT_LEVEL: i32 = 1;
81
82 /// Creates a new zstd compression wrapper using the [default level](Self::DEFAULT_LEVEL).
83 pub fn new() -> Self {
84 Self::with_level(Self::DEFAULT_LEVEL)
85 }
86
87 /// Creates a new zstd compression wrapper using the specified compression level.
88 ///
89 /// Higher levels compress better but are slower. Negative levels are faster than
90 /// level 1 but compress worse. The level is clamped to the range supported by zstd.
91 pub fn with_level(level: i32) -> Self {
92 Self { level, window_log: None }
93 }
94
95 /// The compression level.
96 pub fn level(&self) -> i32 {
97 self.level
98 }
99
100 /// Sets the base 2 logarithm of the maximum window size.
101 ///
102 /// This limits the memory used for compressing and decompressing each link.
103 /// Received data that was compressed using a larger window cannot be decompressed,
104 /// thus both ends must use the same setting.
105 ///
106 /// If `None` (default), the window size is determined by the compression level
107 /// for compressing and the default limit of zstd (128 MiB) applies to decompressing.
108 pub fn set_window_log(&mut self, window_log: Option<u32>) {
109 self.window_log = window_log;
110 }
111
112 /// Wraps the stream of a link.
113 fn wrap(&self, stream: StreamBox) -> Result<StreamBox> {
114 let StreamBox::Io(io) = stream else {
115 return Err(Error::new(ErrorKind::Unsupported, "zstd wrapper only supports IO-based streams"));
116 };
117 let (read, write) = io.into_split();
118
119 let cparams: Vec<_> = self.window_log.map(CParameter::window_log).into_iter().collect();
120 let encoder = ZstdEncoder::with_quality_and_params(write, Level::Precise(self.level), &cparams);
121
122 let dparams: Vec<_> = self.window_log.map(DParameter::window_log_max).into_iter().collect();
123 let mut decoder = ZstdDecoder::with_params(BufReader::new(read), &dparams);
124 decoder.multiple_members(true);
125
126 Ok(IoBox::new(decoder, encoder).into())
127 }
128}
129
130#[async_trait]
131impl ConnectingWrapper for Zstd {
132 fn name(&self) -> &str {
133 NAME
134 }
135
136 async fn wrap(&self, stream: StreamBox) -> Result<StreamBox> {
137 Zstd::wrap(self, stream)
138 }
139}
140
141#[async_trait]
142impl AcceptingWrapper for Zstd {
143 fn name(&self) -> &str {
144 NAME
145 }
146
147 async fn wrap(&self, stream: StreamBox) -> Result<StreamBox> {
148 Zstd::wrap(self, stream)
149 }
150}
151
152#[cfg(test)]
153mod tests;