Mountain/IPC/Encryption/
MessageCompressor.rs

1//! # Message Compressor (IPC Encryption)
2//!
3//! ## RESPONSIBILITIES
4//! This module provides message compression using Gzip to optimize IPC message
5//! transfer. It reduces payload size for better performance, especially for
6//! large messages or high-frequency communication.
7//!
8//! ## ARCHITECTURAL ROLE
9//! This module is part of the performance optimization layer in the IPC
10//! architecture, reducing bandwidth usage and improving transfer speeds.
11//!
12//! ## KEY COMPONENTS
13//!
14//! - **MessageCompressor**: Main compression structure with configurable
15//!   compression level
16//!
17//! ## ERROR HANDLING
18//! Compression and decompression operations return Result types with
19//! descriptive error messages for failures.
20//!
21//! ## LOGGING
22//! Debug-level logging for compression statistics, error for failures.
23//!
24//! ## PERFORMANCE CONSIDERATIONS
25//! - Compression level 6 provides good balance between speed and ratio
26//! - Batch size 10 aggregates small messages for efficiency
27//! - Gzip provides widely compatible compression format
28//!
29//! ## TODO
30//! - Add compression algorithm selection (LZ4, Zstd)
31//! - Implement adaptive compression based on message size
32//! - Add compression ratio tracking and optimization
33//! - Implement streaming compression for very large messages
34
35use std::io::{Read, Write};
36
37use flate2::{Compression, read::GzDecoder, write::GzEncoder};
38use log::debug;
39
40use super::super::Message::TauriIPCMessage;
41
42/// Message compression utility for optimizing IPC message transfer
43///
44/// This structure provides Gzip-based compression to reduce the size of IPC
45/// messages, improving transfer speed and reducing bandwidth usage.
46///
47/// ## Compression Flow
48///
49/// ```text
50/// Multiple TauriIPCMessage
51///     |
52///     | 1. Serialize to JSON
53///     v
54/// Serialized JSON bytes
55///     |
56///     | 2. Compress with Gzip
57///     v
58/// Compressed bytes (smaller)
59///     |
60///     | 3. Base64 encode for transport
61///     v
62/// Base64 string (sendable via IPC)
63/// ```
64///
65/// ## Decompression Flow
66///
67/// ```text
68/// Base64 string (received via IPC)
69///     |
70///     | 1. Base64 decode
71///     v
72/// Compressed bytes
73///     |
74///     | 2. Decompress with Gzip
75///     v
76/// Serialized JSON bytes
77///     |
78///     | 3. Deserialize to TauriIPCMessage[]
79///     v
80/// Multiple TauriIPCMessage
81/// ```
82///
83/// ## Compression Levels
84///
85/// Compression levels range from 0 (fastest, no compression) to 9 (slowest,
86/// best compression). The recommended level is 6 for a good balance.
87///
88/// | Level | Speed | Ratio | Use Case |
89/// |-------|-------|-------|----------|
90/// | 0 | Fastest | 1:1 | Testing/debugging |
91/// | 1-3 | Fast | 2:1-3:1 | Real-time systems |
92/// | 4-6 | Medium | 3:1-5:1 | General use |
93/// | 7-9 | Slow | 5:1-7:1 | Bandwidth-constrained |
94///
95/// ## Example Usage
96///
97/// ```rust,ignore
98/// let compressor = MessageCompressor::new(6, 10);
99///
100/// // Compress messages
101/// let messages = vec![message1, message2, message3];
102/// let compressed = compressor.compress_messages(messages)?;
103///
104/// // Decompress messages
105/// let decompressed = compressor.decompress_messages(&compressed)?;
106/// ```
107pub struct MessageCompressor {
108	/// Gzip compression level (0-9, where 0 is no compression)
109	CompressionLevel:u32,
110
111	/// Minimum number of messages required for batch processing
112	BatchSize:usize,
113}
114
115impl MessageCompressor {
116	/// Create a new message compressor with specified parameters
117	///
118	/// ## Parameters
119	/// - `CompressionLevel`: Gzip compression level (0-9, default 6)
120	/// - `BatchSize`: Minimum messages for batch processing (default 10)
121	///
122	/// ## Example
123	///
124	/// ```rust,ignore
125	/// let compressor = MessageCompressor::new(6, 10);
126	/// ```
127	pub fn new(CompressionLevel:u32, BatchSize:usize) -> Self {
128		debug!(
129			"[MessageCompressor] Created with level: {}, batch size: {}",
130			CompressionLevel, BatchSize
131		);
132		Self { CompressionLevel, BatchSize }
133	}
134
135	/// Compress messages using Gzip for efficient transfer
136	///
137	/// This method serializes multiple messages to JSON and compresses them
138	/// using Gzip, significantly reducing the payload size.
139	///
140	/// ## Parameters
141	/// - `Messages`: Vector of TauriIPCMessage to compress
142	///
143	/// ## Returns
144	/// - `Ok(Vec<u8>)`: Compressed message data
145	/// - `Err(String)`: Error message if compression fails
146	///
147	/// ## Example
148	///
149	/// ```rust,ignore
150	/// let messages = vec![msg1, msg2, msg3];
151	/// let compressed = compressor.compress_messages(messages)?;
152	/// ```
153	pub fn compress_messages(&self, Messages:Vec<TauriIPCMessage>) -> Result<Vec<u8>, String> {
154		debug!("[MessageCompressor] Compressing {} messages", Messages.len());
155
156		// Serialize messages to JSON
157		let SerializedMessages =
158			serde_json::to_vec(&Messages).map_err(|e| format!("Failed to serialize messages: {}", e))?;
159
160		let original_size = SerializedMessages.len();
161
162		// Compress using Gzip
163		let mut encoder = GzEncoder::new(Vec::new(), Compression::new(self.CompressionLevel));
164		encoder
165			.write_all(&SerializedMessages)
166			.map_err(|e| format!("Failed to compress messages: {}", e))?;
167
168		let compressed_data = encoder.finish().map_err(|e| format!("Failed to finish compression: {}", e))?;
169
170		let compressed_size = compressed_data.len();
171		let ratio = if original_size > 0 {
172			(compressed_size as f64 / original_size as f64) * 100.0
173		} else {
174			100.0
175		};
176
177		debug!(
178			"[MessageCompressor] Compression complete: {} -> {} bytes ({:.1}%)",
179			original_size, compressed_size, ratio
180		);
181
182		Ok(compressed_data)
183	}
184
185	/// Decompress messages from compressed data
186	///
187	/// This method decompresses Gzip-compressed data and deserializes it back
188	/// into TauriIPCMessage objects.
189	///
190	/// ## Parameters
191	/// - `CompressedData`: Compressed message data
192	///
193	/// ## Returns
194	/// - `Ok(Vec<TauriIPCMessage>)`: Decompressed messages
195	/// - `Err(String)`: Error message if decompression fails
196	///
197	/// ## Example
198	///
199	/// ```rust,ignore
200	/// let messages = compressor.decompress_messages(&compressed_data)?;
201	/// ```
202	pub fn decompress_messages(&self, CompressedData:&[u8]) -> Result<Vec<TauriIPCMessage>, String> {
203		debug!("[MessageCompressor] Decompressing {} bytes", CompressedData.len());
204
205		let compressed_size = CompressedData.len();
206
207		// Decompress using Gzip
208		let mut decoder = GzDecoder::new(CompressedData);
209		let mut DecompressedData = Vec::new();
210		decoder
211			.read_to_end(&mut DecompressedData)
212			.map_err(|e| format!("Failed to decompress data: {}", e))?;
213
214		let decompressed_size = DecompressedData.len();
215
216		// Deserialize messages with explicit type annotation
217		let messages:Vec<TauriIPCMessage> =
218			serde_json::from_slice(&DecompressedData).map_err(|e| format!("Failed to deserialize messages: {}", e))?;
219
220		debug!(
221			"[MessageCompressor] Decompression complete: {} -> {} bytes, {} messages",
222			compressed_size,
223			decompressed_size,
224			messages.len()
225		);
226
227		Ok(messages)
228	}
229
230	/// Check if messages should be batched for compression
231	///
232	/// This method determines if the number of messages meets the threshold
233	/// for batch compression.
234	///
235	/// ## Parameters
236	/// - `MessagesCount`: Number of messages to check
237	///
238	/// ## Returns
239	/// - `true`: Should batch (meets minimum threshold)
240	/// - `false`: Should not batch (below threshold)
241	///
242	/// ## Example
243	///
244	/// ```rust,ignore
245	/// if compressor.should_batch(messages.len()) {
246	///     // Batch compress
247	/// } else {
248	///     // Send individually
249	/// }
250	/// ```
251	pub fn should_batch(&self, MessagesCount:usize) -> bool {
252		let should_batch = MessagesCount >= self.BatchSize;
253		debug!(
254			"[MessageCompressor] Batch check: {} >= {} = {}",
255			MessagesCount, self.BatchSize, should_batch
256		);
257		should_batch
258	}
259
260	/// Get the compression level
261	pub fn compression_level(&self) -> u32 { self.CompressionLevel }
262
263	/// Get the batch size threshold
264	pub fn batch_size(&self) -> usize { self.BatchSize }
265
266	/// Create a compressor with default settings (level 6, batch size 10)
267	pub fn default() -> Self { Self::new(6, 10) }
268
269	/// Create a fast compressor (level 3, batch size 5)
270	pub fn fast() -> Self { Self::new(3, 5) }
271
272	/// Create a maximum compression compressor (level 9, batch size 20)
273	pub fn max() -> Self { Self::new(9, 20) }
274}
275
276#[cfg(test)]
277#[allow(unused_imports)]
278mod tests {
279	use super::*;
280
281	fn create_test_message(id:u32) -> TauriIPCMessage {
282		TauriIPCMessage::new(
283			format!("test_channel_{}", id),
284			serde_json::json!({
285				"id": id,
286				"data": "test data that should compress well when repeated many times across multiple messages".repeat(10)
287			}),
288			Some("test_sender".to_string()),
289		)
290	}
291
292	#[test]
293	fn test_compressor_creation() {
294		let compressor = MessageCompressor::new(6, 10);
295		assert_eq!(compressor.compression_level(), 6);
296		assert_eq!(compressor.batch_size(), 10);
297	}
298
299	#[test]
300	fn test_default_compressor() {
301		let compressor = MessageCompressor::default();
302		assert_eq!(compressor.compression_level(), 6);
303		assert_eq!(compressor.batch_size(), 10);
304	}
305
306	#[test]
307	fn test_fast_compressor() {
308		let compressor = MessageCompressor::fast();
309		assert_eq!(compressor.compression_level(), 3);
310		assert_eq!(compressor.batch_size(), 5);
311	}
312
313	#[test]
314	fn test_max_compressor() {
315		let compressor = MessageCompressor::max();
316		assert_eq!(compressor.compression_level(), 9);
317		assert_eq!(compressor.batch_size(), 20);
318	}
319
320	#[test]
321	fn test_should_batch() {
322		let compressor = MessageCompressor::new(6, 10);
323		assert!(!compressor.should_batch(5));
324		assert!(compressor.should_batch(10));
325		assert!(compressor.should_batch(15));
326	}
327
328	#[test]
329	fn test_compress_and_decompress() {
330		let compressor = MessageCompressor::default();
331		let original_messages = vec![create_test_message(1), create_test_message(2), create_test_message(3)];
332
333		// Compress
334		let compressed = compressor.compress_messages(original_messages.clone()).unwrap();
335		assert!(!compressed.is_empty());
336
337		// Decompress
338		let decompressed = compressor.decompress_messages(&compressed).unwrap();
339		assert_eq!(decompressed.len(), original_messages.len());
340
341		// Verify content
342		for i in 0..original_messages.len() {
343			assert_eq!(decompressed[i].channel, original_messages[i].channel);
344		}
345	}
346
347	#[test]
348	fn test_compression_ratio() {
349		let compressor = MessageCompressor::default();
350
351		// Create large messages that should compress well
352		let messages:Vec<TauriIPCMessage> = (0..20).map(|i| create_test_message(i)).collect();
353
354		let compressed = compressor.compress_messages(messages.clone()).unwrap();
355
356		// Compressed size should be significantly smaller
357		let original_data = serde_json::to_vec(&messages).unwrap();
358		assert!(compressed.len() < original_data.len());
359	}
360
361	#[test]
362	fn test_empty_messages() {
363		let compressor = MessageCompressor::default();
364		let messages = vec![];
365
366		let compressed = compressor.compress_messages(messages).unwrap();
367		let decompressed = compressor.decompress_messages(&compressed).unwrap();
368
369		assert!(decompressed.is_empty());
370	}
371
372	#[test]
373	fn test_single_message() {
374		let compressor = MessageCompressor::default();
375		let messages = vec![create_test_message(1)];
376
377		let compressed = compressor.compress_messages(messages.clone()).unwrap();
378		let decompressed = compressor.decompress_messages(&compressed).unwrap();
379
380		assert_eq!(decompressed.len(), 1);
381		assert_eq!(decompressed[0].channel, messages[0].channel);
382	}
383}