Skip to main content

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};
38
39use super::super::Message::TauriIPCMessage;
40use crate::dev_log;
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		dev_log!(
129			"encryption",
130			"[MessageCompressor] Created with level: {}, batch size: {}",
131			CompressionLevel,
132			BatchSize
133		);
134		Self { CompressionLevel, BatchSize }
135	}
136
137	/// Compress messages using Gzip for efficient transfer
138	///
139	/// This method serializes multiple messages to JSON and compresses them
140	/// using Gzip, significantly reducing the payload size.
141	///
142	/// ## Parameters
143	/// - `Messages`: Vector of TauriIPCMessage to compress
144	///
145	/// ## Returns
146	/// - `Ok(Vec<u8>)`: Compressed message data
147	/// - `Err(String)`: Error message if compression fails
148	///
149	/// ## Example
150	///
151	/// ```rust,ignore
152	/// let messages = vec![msg1, msg2, msg3];
153	/// let compressed = compressor.compress_messages(messages)?;
154	/// ```
155	pub fn compress_messages(&self, Messages:Vec<TauriIPCMessage>) -> Result<Vec<u8>, String> {
156		dev_log!("encryption", "[MessageCompressor] Compressing {} messages", Messages.len());
157
158		// Serialize messages to JSON
159		let SerializedMessages =
160			serde_json::to_vec(&Messages).map_err(|e| format!("Failed to serialize messages: {}", e))?;
161
162		let original_size = SerializedMessages.len();
163
164		// Compress using Gzip
165		let mut encoder = GzEncoder::new(Vec::new(), Compression::new(self.CompressionLevel));
166		encoder
167			.write_all(&SerializedMessages)
168			.map_err(|e| format!("Failed to compress messages: {}", e))?;
169
170		let compressed_data = encoder.finish().map_err(|e| format!("Failed to finish compression: {}", e))?;
171
172		let compressed_size = compressed_data.len();
173		let ratio = if original_size > 0 {
174			(compressed_size as f64 / original_size as f64) * 100.0
175		} else {
176			100.0
177		};
178
179		dev_log!(
180			"encryption",
181			"[MessageCompressor] Compression complete: {} -> {} bytes ({:.1}%)",
182			original_size,
183			compressed_size,
184			ratio
185		);
186
187		Ok(compressed_data)
188	}
189
190	/// Decompress messages from compressed data
191	///
192	/// This method decompresses Gzip-compressed data and deserializes it back
193	/// into TauriIPCMessage objects.
194	///
195	/// ## Parameters
196	/// - `CompressedData`: Compressed message data
197	///
198	/// ## Returns
199	/// - `Ok(Vec<TauriIPCMessage>)`: Decompressed messages
200	/// - `Err(String)`: Error message if decompression fails
201	///
202	/// ## Example
203	///
204	/// ```rust,ignore
205	/// let messages = compressor.decompress_messages(&compressed_data)?;
206	/// ```
207	pub fn decompress_messages(&self, CompressedData:&[u8]) -> Result<Vec<TauriIPCMessage>, String> {
208		dev_log!("encryption", "[MessageCompressor] Decompressing {} bytes", CompressedData.len());
209
210		let compressed_size = CompressedData.len();
211
212		// Decompress using Gzip
213		let mut decoder = GzDecoder::new(CompressedData);
214		let mut DecompressedData = Vec::new();
215		decoder
216			.read_to_end(&mut DecompressedData)
217			.map_err(|e| format!("Failed to decompress data: {}", e))?;
218
219		let decompressed_size = DecompressedData.len();
220
221		// Deserialize messages with explicit type annotation
222		let messages:Vec<TauriIPCMessage> =
223			serde_json::from_slice(&DecompressedData).map_err(|e| format!("Failed to deserialize messages: {}", e))?;
224
225		dev_log!(
226			"encryption",
227			"[MessageCompressor] Decompression complete: {} -> {} bytes, {} messages",
228			compressed_size,
229			decompressed_size,
230			messages.len()
231		);
232
233		Ok(messages)
234	}
235
236	/// Check if messages should be batched for compression
237	///
238	/// This method determines if the number of messages meets the threshold
239	/// for batch compression.
240	///
241	/// ## Parameters
242	/// - `MessagesCount`: Number of messages to check
243	///
244	/// ## Returns
245	/// - `true`: Should batch (meets minimum threshold)
246	/// - `false`: Should not batch (below threshold)
247	///
248	/// ## Example
249	///
250	/// ```rust,ignore
251	/// if compressor.should_batch(messages.len()) {
252	///     // Batch compress
253	/// } else {
254	///     // Send individually
255	/// }
256	/// ```
257	pub fn should_batch(&self, MessagesCount:usize) -> bool {
258		let should_batch = MessagesCount >= self.BatchSize;
259		dev_log!(
260			"encryption",
261			"[MessageCompressor] Batch check: {} >= {} = {}",
262			MessagesCount,
263			self.BatchSize,
264			should_batch
265		);
266		should_batch
267	}
268
269	/// Get the compression level
270	pub fn compression_level(&self) -> u32 { self.CompressionLevel }
271
272	/// Get the batch size threshold
273	pub fn batch_size(&self) -> usize { self.BatchSize }
274
275	/// Create a compressor with default settings (level 6, batch size 10)
276	pub fn default() -> Self { Self::new(6, 10) }
277
278	/// Create a fast compressor (level 3, batch size 5)
279	pub fn fast() -> Self { Self::new(3, 5) }
280
281	/// Create a maximum compression compressor (level 9, batch size 20)
282	pub fn max() -> Self { Self::new(9, 20) }
283}
284
285#[cfg(test)]
286#[allow(unused_imports)]
287mod tests {
288	use super::*;
289
290	fn create_test_message(id:u32) -> TauriIPCMessage {
291		TauriIPCMessage::new(
292			format!("test_channel_{}", id),
293			serde_json::json!({
294				"id": id,
295				"data": "test data that should compress well when repeated many times across multiple messages".repeat(10)
296			}),
297			Some("test_sender".to_string()),
298		)
299	}
300
301	#[test]
302	fn test_compressor_creation() {
303		let compressor = MessageCompressor::new(6, 10);
304		assert_eq!(compressor.compression_level(), 6);
305		assert_eq!(compressor.batch_size(), 10);
306	}
307
308	#[test]
309	fn test_default_compressor() {
310		let compressor = MessageCompressor::default();
311		assert_eq!(compressor.compression_level(), 6);
312		assert_eq!(compressor.batch_size(), 10);
313	}
314
315	#[test]
316	fn test_fast_compressor() {
317		let compressor = MessageCompressor::fast();
318		assert_eq!(compressor.compression_level(), 3);
319		assert_eq!(compressor.batch_size(), 5);
320	}
321
322	#[test]
323	fn test_max_compressor() {
324		let compressor = MessageCompressor::max();
325		assert_eq!(compressor.compression_level(), 9);
326		assert_eq!(compressor.batch_size(), 20);
327	}
328
329	#[test]
330	fn test_should_batch() {
331		let compressor = MessageCompressor::new(6, 10);
332		assert!(!compressor.should_batch(5));
333		assert!(compressor.should_batch(10));
334		assert!(compressor.should_batch(15));
335	}
336
337	#[test]
338	fn test_compress_and_decompress() {
339		let compressor = MessageCompressor::default();
340		let original_messages = vec![create_test_message(1), create_test_message(2), create_test_message(3)];
341
342		// Compress
343		let compressed = compressor.compress_messages(original_messages.clone()).unwrap();
344		assert!(!compressed.is_empty());
345
346		// Decompress
347		let decompressed = compressor.decompress_messages(&compressed).unwrap();
348		assert_eq!(decompressed.len(), original_messages.len());
349
350		// Verify content
351		for i in 0..original_messages.len() {
352			assert_eq!(decompressed[i].channel, original_messages[i].channel);
353		}
354	}
355
356	#[test]
357	fn test_compression_ratio() {
358		let compressor = MessageCompressor::default();
359
360		// Create large messages that should compress well
361		let messages:Vec<TauriIPCMessage> = (0..20).map(|i| create_test_message(i)).collect();
362
363		let compressed = compressor.compress_messages(messages.clone()).unwrap();
364
365		// Compressed size should be significantly smaller
366		let original_data = serde_json::to_vec(&messages).unwrap();
367		assert!(compressed.len() < original_data.len());
368	}
369
370	#[test]
371	fn test_empty_messages() {
372		let compressor = MessageCompressor::default();
373		let messages = vec![];
374
375		let compressed = compressor.compress_messages(messages).unwrap();
376		let decompressed = compressor.decompress_messages(&compressed).unwrap();
377
378		assert!(decompressed.is_empty());
379	}
380
381	#[test]
382	fn test_single_message() {
383		let compressor = MessageCompressor::default();
384		let messages = vec![create_test_message(1)];
385
386		let compressed = compressor.compress_messages(messages.clone()).unwrap();
387		let decompressed = compressor.decompress_messages(&compressed).unwrap();
388
389		assert_eq!(decompressed.len(), 1);
390		assert_eq!(decompressed[0].channel, messages[0].channel);
391	}
392}