1use std::{
9 io::{Read, Write},
10 str::FromStr,
11};
12
13use crate::status::Status;
14
15pub const DEFAULT_MAX_DECODING_MESSAGE_SIZE: usize = 16 * 1024 * 1024;
17pub const DEFAULT_MIN_COMPRESS_SIZE_BYTES: usize = 256;
19
20#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
22pub enum CompressionAlgorithm {
23 #[default]
25 Zstd,
26 Lz4,
28 Gzip,
30 Deflate,
32 Snappy,
34 Identity,
36}
37
38impl CompressionAlgorithm {
39 pub const fn as_str(self) -> &'static str {
41 match self {
42 Self::Zstd => "zstd",
43 Self::Lz4 => "lz4",
44 Self::Gzip => "gzip",
45 Self::Deflate => "deflate",
46 Self::Snappy => "snappy",
47 Self::Identity => "identity",
48 }
49 }
50
51 pub fn from_encoding(encoding: &str) -> Result<Self, Status> {
56 match encoding {
57 "zstd" => Ok(Self::Zstd),
58 "lz4" => Ok(Self::Lz4),
59 "gzip" => Ok(Self::Gzip),
60 "deflate" => Ok(Self::Deflate),
61 "snappy" => Ok(Self::Snappy),
62 "identity" => Ok(Self::Identity),
63 _ => Err(Status::unimplemented("unsupported grpc-encoding")),
64 }
65 }
66
67 pub fn compress(self, input: &[u8]) -> Result<Vec<u8>, Status> {
72 let failed = |_| Status::internal("message compression failed");
73 match self {
74 Self::Identity => Ok(input.to_vec()),
75 Self::Zstd => zstd::stream::encode_all(input, 1).map_err(failed),
76 Self::Lz4 => {
77 let mut encoder = lz4_flex::frame::FrameEncoder::new(Vec::new());
78 encoder.write_all(input).map_err(failed)?;
79 encoder
80 .finish()
81 .map_err(|_| Status::internal("LZ4 compression failed"))
82 }
83 Self::Gzip => {
84 let mut encoder =
85 flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::fast());
86 encoder.write_all(input).map_err(failed)?;
87 encoder.finish().map_err(failed)
88 }
89 Self::Deflate => {
90 let mut encoder =
91 flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::fast());
92 encoder.write_all(input).map_err(failed)?;
93 encoder.finish().map_err(failed)
94 }
95 Self::Snappy => snap::raw::Encoder::new()
96 .compress_vec(input)
97 .map_err(|_| Status::internal("Snappy compression failed")),
98 }
99 }
100
101 pub fn decompress(self, input: &[u8], max_size: usize) -> Result<Vec<u8>, Status> {
112 match self {
113 Self::Identity => copy_bounded(input, max_size),
114 Self::Zstd => {
115 let mut decoder =
116 zstd::stream::read::Decoder::new(input).map_err(|_| malformed())?;
117 decoder.window_log_max(24).map_err(|_| malformed())?;
118 read_bounded(decoder, max_size)
119 }
120 Self::Lz4 => {
121 validate_lz4_extent(input)?;
122 read_bounded(
123 Lz4MessageReader(lz4_flex::frame::FrameDecoder::new(StrictFrameReader(input))),
124 max_size,
125 )
126 }
127 Self::Gzip => read_bounded(flate2::read::MultiGzDecoder::new(input), max_size),
128 Self::Deflate => decode_deflate(input, max_size),
129 Self::Snappy => {
130 let size = snap::raw::decompress_len(input).map_err(|_| malformed())?;
131 if size > max_size {
132 return Err(too_large());
133 }
134 let mut output = vec![0; size];
135 let written = snap::raw::Decoder::new()
136 .decompress(input, &mut output)
137 .map_err(|_| malformed())?;
138 output.truncate(written);
139 Ok(output)
140 }
141 }
142 }
143}
144
145impl FromStr for CompressionAlgorithm {
146 type Err = Status;
147
148 fn from_str(value: &str) -> Result<Self, Self::Err> {
149 Self::from_encoding(value)
150 }
151}
152
153#[derive(Clone, Debug, Eq, PartialEq)]
155pub struct CompressedMessage {
156 pub compressed: bool,
157 pub data: Vec<u8>,
158}
159
160#[derive(Clone, Copy, Debug, Eq, PartialEq)]
162pub struct CompressionConfig {
163 pub algorithm: CompressionAlgorithm,
164 pub min_compress_size_bytes: usize,
165 pub max_decoding_message_size: usize,
166}
167
168impl Default for CompressionConfig {
169 fn default() -> Self {
170 Self {
171 algorithm: CompressionAlgorithm::default(),
172 min_compress_size_bytes: DEFAULT_MIN_COMPRESS_SIZE_BYTES,
173 max_decoding_message_size: DEFAULT_MAX_DECODING_MESSAGE_SIZE,
174 }
175 }
176}
177
178impl CompressionConfig {
179 pub fn compress(self, input: &[u8]) -> Result<CompressedMessage, Status> {
184 if self.algorithm != CompressionAlgorithm::Identity
185 && input.len() >= self.min_compress_size_bytes
186 {
187 let data = self.algorithm.compress(input)?;
188 if data.len() < input.len() {
189 return Ok(CompressedMessage {
190 compressed: true,
191 data,
192 });
193 }
194 }
195 Ok(CompressedMessage {
196 compressed: false,
197 data: input.to_vec(),
198 })
199 }
200
201 pub fn decompress(self, input: &[u8], compressed: bool) -> Result<Vec<u8>, Status> {
208 if !compressed {
209 return copy_bounded(input, self.max_decoding_message_size);
210 }
211 if self.algorithm == CompressionAlgorithm::Identity {
212 return Err(Status::internal(
213 "compressed flag requires a non-identity encoding",
214 ));
215 }
216 self.algorithm
217 .decompress(input, self.max_decoding_message_size)
218 }
219}
220
221pub fn negotiate_compression(
226 preferred: &[CompressionAlgorithm],
227 peer_accept_encoding: &str,
228) -> CompressionAlgorithm {
229 preferred
230 .iter()
231 .copied()
232 .find(|algorithm| {
233 *algorithm == CompressionAlgorithm::Identity
234 || peer_accept_encoding
235 .split(',')
236 .any(|token| token.trim() == algorithm.as_str())
237 })
238 .unwrap_or(CompressionAlgorithm::Identity)
239}
240
241pub fn accept_encoding_header(algorithms: &[CompressionAlgorithm]) -> String {
243 let mut header = String::new();
244 for (index, algorithm) in algorithms.iter().enumerate() {
245 if algorithms[..index].contains(algorithm) {
246 continue;
247 }
248 if !header.is_empty() {
249 header.push(',');
250 }
251 header.push_str(algorithm.as_str());
252 }
253 header
254}
255
256struct StrictFrameReader<'a>(&'a [u8]);
259
260struct Lz4MessageReader<'a>(lz4_flex::frame::FrameDecoder<StrictFrameReader<'a>>);
263
264impl Read for Lz4MessageReader<'_> {
265 fn read(&mut self, output: &mut [u8]) -> std::io::Result<usize> {
266 loop {
267 let count = self.0.read(output)?;
268 if count != 0 || output.is_empty() || self.0.get_ref().0.is_empty() {
269 return Ok(count);
270 }
271 }
272 }
273}
274
275fn validate_lz4_extent(input: &[u8]) -> Result<(), Status> {
278 if input.get(..4) != Some(&[0x04, 0x22, 0x4d, 0x18]) {
279 return Err(malformed());
280 }
281 let flags = *input.get(4).ok_or_else(malformed)?;
282 let header_size = 7 + usize::from(flags & 8 != 0) * 8 + usize::from(flags & 1 != 0) * 4;
283 let mut remaining = input.get(header_size..).ok_or_else(malformed)?;
284 loop {
285 let header = remaining.get(..4).ok_or_else(malformed)?;
286 let size = u32::from_le_bytes([header[0], header[1], header[2], header[3]]);
287 remaining = &remaining[4..];
288 if size == 0 {
289 let checksum_size = usize::from(flags & 4 != 0) * 4;
290 return if remaining.len() == checksum_size {
291 Ok(())
292 } else {
293 Err(malformed())
294 };
295 }
296 let data_size = usize::try_from(size & 0x7fff_ffff).map_err(|_| malformed())?;
297 let block_size = data_size
298 .checked_add(usize::from(flags & 16 != 0) * 4)
299 .ok_or_else(malformed)?;
300 remaining = remaining.get(block_size..).ok_or_else(malformed)?;
301 }
302}
303
304impl Read for StrictFrameReader<'_> {
305 fn read(&mut self, output: &mut [u8]) -> std::io::Result<usize> {
306 if self.0.is_empty() && !output.is_empty() {
307 return Err(std::io::Error::new(
308 std::io::ErrorKind::InvalidData,
309 "missing LZ4 end marker",
310 ));
311 }
312 self.0.read(output)
313 }
314}
315
316fn decode_deflate(input: &[u8], max_size: usize) -> Result<Vec<u8>, Status> {
317 let mut decoder = flate2::Decompress::new(true);
318 let mut output = Vec::new();
319 let mut chunk = [0; 8192];
320 loop {
321 let before_in = decoder.total_in();
322 let before_out = decoder.total_out();
323 let remaining = max_size - output.len();
324 let wanted = chunk.len().min(remaining.saturating_add(1));
325 let state = decoder
326 .decompress(
327 &input[before_in as usize..],
328 &mut chunk[..wanted],
329 flate2::FlushDecompress::None,
330 )
331 .map_err(|_| malformed())?;
332 let count = (decoder.total_out() - before_out) as usize;
333 if count > remaining {
334 return Err(too_large());
335 }
336 output.extend_from_slice(&chunk[..count]);
337 if state == flate2::Status::StreamEnd {
338 if decoder.total_in() != input.len() as u64 {
339 return Err(malformed());
340 }
341 return Ok(output);
342 }
343 if before_in == decoder.total_in() && count == 0 {
344 return Err(malformed());
345 }
346 }
347}
348
349fn copy_bounded(input: &[u8], max_size: usize) -> Result<Vec<u8>, Status> {
350 if input.len() > max_size {
351 return Err(too_large());
352 }
353 Ok(input.to_vec())
354}
355
356fn read_bounded(mut reader: impl Read, max_size: usize) -> Result<Vec<u8>, Status> {
357 let mut output = Vec::new();
358 let mut chunk = [0; 8192];
359 loop {
360 let remaining = max_size - output.len();
361 let wanted = chunk.len().min(remaining.saturating_add(1));
362 let count = reader.read(&mut chunk[..wanted]).map_err(|error| {
363 if error.to_string() == "Frame requires too much memory for decoding" {
366 Status::resource_exhausted("Zstd decoding window exceeds 16 MiB")
367 } else {
368 malformed()
369 }
370 })?;
371 if count == 0 {
372 return Ok(output);
373 }
374 if count > remaining {
375 return Err(too_large());
376 }
377 output.extend_from_slice(&chunk[..count]);
378 }
379}
380
381fn too_large() -> Status {
382 Status::resource_exhausted("decoded message exceeds configured size limit")
383}
384
385fn malformed() -> Status {
386 Status::internal("malformed compressed message")
387}
388
389#[cfg(test)]
390mod unit_tests {
391 use super::*;
392 use crate::status::Code;
393
394 const ALGORITHMS: [CompressionAlgorithm; 6] = [
395 CompressionAlgorithm::Zstd,
396 CompressionAlgorithm::Lz4,
397 CompressionAlgorithm::Gzip,
398 CompressionAlgorithm::Deflate,
399 CompressionAlgorithm::Snappy,
400 CompressionAlgorithm::Identity,
401 ];
402
403 #[test]
404 fn all_algorithms_round_trip_and_enforce_exact_limit() {
405 let payload = vec![b'x'; 32768];
406 for algorithm in ALGORITHMS {
407 let encoded = algorithm.compress(&payload).unwrap();
408 assert_eq!(
409 algorithm.decompress(&encoded, payload.len()).unwrap(),
410 payload
411 );
412 assert_eq!(
413 algorithm
414 .decompress(&encoded, payload.len() - 1)
415 .unwrap_err()
416 .code(),
417 Code::ResourceExhausted
418 );
419 let empty = algorithm.compress(&[]).unwrap();
420 assert_eq!(algorithm.decompress(&empty, 0).unwrap(), b"");
421 }
422 }
423
424 #[test]
425 fn adaptive_threshold_and_negative_compression_keep_flag_clear() {
426 for algorithm in ALGORITHMS {
427 let config = CompressionConfig {
428 algorithm,
429 ..CompressionConfig::default()
430 };
431 let small = vec![b'x'; 255];
432 let output = config.compress(&small).unwrap();
433 assert!(!output.compressed);
434 assert_eq!(output.data, small);
435 let output = config.compress(&vec![b'x'; 256]).unwrap();
436 assert_eq!(
437 output.compressed,
438 algorithm != CompressionAlgorithm::Identity
439 );
440 assert_eq!(
441 config.decompress(&output.data, output.compressed).unwrap(),
442 vec![b'x'; 256]
443 );
444 let config = CompressionConfig {
445 min_compress_size_bytes: 0,
446 ..config
447 };
448 let output = config.compress(b"a").unwrap();
449 assert!(!output.compressed);
450 assert_eq!(output.data, b"a");
451 }
452 }
453
454 #[test]
455 fn scenario_high_expansion_streams_stop_at_small_limit() {
456 let payload = vec![0; 1024 * 1024];
457 for algorithm in ALGORITHMS {
458 let encoded = algorithm.compress(&payload).unwrap();
459 assert_eq!(
460 algorithm.decompress(&encoded, 1024).unwrap_err().code(),
461 Code::ResourceExhausted
462 );
463 }
464 }
465
466 #[test]
467 fn scenario_default_limit_rejects_large_output_and_zstd_window() {
468 let config = CompressionConfig::default();
469 let payload = vec![0; DEFAULT_MAX_DECODING_MESSAGE_SIZE + 1];
470 let encoded = config.algorithm.compress(&payload).unwrap();
471 assert_eq!(
472 config.decompress(&encoded, true).unwrap_err().code(),
473 Code::ResourceExhausted
474 );
475 let large_window = [0x28, 0xb5, 0x2f, 0xfd, 0, 0x78, 1, 0, 0];
477 assert_eq!(
478 config.decompress(&large_window, true).unwrap_err().code(),
479 Code::ResourceExhausted
480 );
481 }
482
483 #[test]
484 fn scenario_corrupt_compressed_input_returns_status() {
485 for algorithm in ALGORITHMS
486 .into_iter()
487 .filter(|a| *a != CompressionAlgorithm::Identity)
488 {
489 let config = CompressionConfig {
490 algorithm,
491 max_decoding_message_size: 1024,
492 ..CompressionConfig::default()
493 };
494 let corrupt = b"not a compressed stream";
495 assert_eq!(
496 algorithm.decompress(corrupt, 1024).unwrap_err().code(),
497 Code::Internal
498 );
499 assert_eq!(
500 config.decompress(corrupt, true).unwrap_err().code(),
501 Code::Internal
502 );
503 }
504 }
505
506 #[test]
507 fn scenario_truncated_streams_are_rejected() {
508 for algorithm in ALGORITHMS
509 .into_iter()
510 .filter(|a| *a != CompressionAlgorithm::Identity)
511 {
512 let encoded = algorithm.compress(&vec![b'x'; 1024]).unwrap();
513 for length in 0..encoded.len() {
514 assert_eq!(
515 algorithm
516 .decompress(&encoded[..length], 2048)
517 .unwrap_err()
518 .code(),
519 Code::Internal,
520 "{algorithm:?} truncated stream of {length}/{} bytes",
521 encoded.len()
522 );
523 }
524 }
525 }
526
527 #[test]
528 fn scenario_trailing_garbage_is_not_silently_discarded() {
529 for algorithm in ALGORITHMS
530 .into_iter()
531 .filter(|a| *a != CompressionAlgorithm::Identity)
532 {
533 let encoded = algorithm.compress(b"message").unwrap();
534 for suffix in [&b"\0"[..], &b"garbage"[..], &[255; 16][..]] {
535 let mut trailing = encoded.clone();
536 trailing.extend_from_slice(suffix);
537 assert_eq!(
538 algorithm.decompress(&trailing, 1024).unwrap_err().code(),
539 Code::Internal,
540 "{algorithm:?} accepted trailing {suffix:?}"
541 );
542 }
543 }
544 }
545
546 #[test]
547 fn scenario_lz4_empty_blocks_require_end_marker_and_preserve_following_data() {
548 let algorithm = CompressionAlgorithm::Lz4;
549 let empty = algorithm.compress(b"").unwrap();
550 let following = algorithm.compress(b"following block").unwrap();
551 for block in [&[0, 0, 0, 0x80][..], &[1, 0, 0, 0, 0][..]] {
552 let mut encoded = empty[..empty.len() - 4].to_vec();
553 encoded.extend_from_slice(block);
554 assert_eq!(
555 algorithm.decompress(&encoded, 64).unwrap_err().code(),
556 Code::Internal
557 );
558 encoded.extend_from_slice(&following[7..]);
559 assert_eq!(
560 algorithm.decompress(&encoded, 64).unwrap(),
561 b"following block"
562 );
563 assert_eq!(
564 algorithm.decompress(&encoded, 14).unwrap_err().code(),
565 Code::ResourceExhausted
566 );
567 }
568 }
569
570 #[test]
571 fn scenario_lz4_optional_fields_and_linked_blocks_round_trip() {
572 let payload = vec![b'x'; 200_000];
573 let info = lz4_flex::frame::FrameInfo::new()
574 .block_size(lz4_flex::frame::BlockSize::Max64KB)
575 .block_mode(lz4_flex::frame::BlockMode::Linked)
576 .content_size(Some(payload.len() as u64))
577 .block_checksums(true)
578 .content_checksum(true);
579 let mut encoder = lz4_flex::frame::FrameEncoder::with_frame_info(info, Vec::new());
580 encoder.write_all(&payload).unwrap();
581 let encoded = encoder.finish().unwrap();
582 assert_eq!(
583 CompressionAlgorithm::Lz4
584 .decompress(&encoded, payload.len())
585 .unwrap(),
586 payload
587 );
588 for length in [encoded.len() - 1, encoded.len() - 4] {
589 assert_eq!(
590 CompressionAlgorithm::Lz4
591 .decompress(&encoded[..length], payload.len())
592 .unwrap_err()
593 .code(),
594 Code::Internal
595 );
596 }
597 let first_block_size = u32::from_le_bytes(encoded[15..19].try_into().unwrap());
598 let block_checksum_offset = 19 + (first_block_size & 0x7fff_ffff) as usize;
599 for offset in [block_checksum_offset, encoded.len() - 1] {
600 let mut corrupt = encoded.clone();
601 corrupt[offset] ^= 1;
602 assert_eq!(
603 CompressionAlgorithm::Lz4
604 .decompress(&corrupt, payload.len())
605 .unwrap_err()
606 .code(),
607 Code::Internal
608 );
609 }
610 }
611
612 #[test]
613 fn scenario_concatenated_streams_preserve_bytes_and_enforce_combined_limit() {
614 for algorithm in [CompressionAlgorithm::Zstd, CompressionAlgorithm::Gzip] {
615 let mut encoded = algorithm.compress(b"first").unwrap();
616 encoded.extend(algorithm.compress(b"second").unwrap());
617 assert_eq!(algorithm.decompress(&encoded, 11).unwrap(), b"firstsecond");
618 assert_eq!(
619 algorithm.decompress(&encoded, 10).unwrap_err().code(),
620 Code::ResourceExhausted
621 );
622 encoded.pop();
623 assert_eq!(
624 algorithm.decompress(&encoded, 11).unwrap_err().code(),
625 Code::Internal
626 );
627 }
628 for algorithm in [
629 CompressionAlgorithm::Lz4,
630 CompressionAlgorithm::Deflate,
631 CompressionAlgorithm::Snappy,
632 ] {
633 let mut encoded = algorithm.compress(b"first").unwrap();
634 encoded.extend(algorithm.compress(b"second").unwrap());
635 assert_eq!(
636 algorithm.decompress(&encoded, 11).unwrap_err().code(),
637 Code::Internal
638 );
639 }
640 }
641
642 #[test]
643 fn uncompressed_frames_obey_limits_and_identity_rejects_compressed_flag() {
644 let config = CompressionConfig {
645 algorithm: CompressionAlgorithm::Identity,
646 max_decoding_message_size: 3,
647 ..CompressionConfig::default()
648 };
649 assert_eq!(
650 config.decompress(b"four", false).unwrap_err().code(),
651 Code::ResourceExhausted
652 );
653 assert_eq!(
654 config.decompress(b"one", true).unwrap_err().code(),
655 Code::Internal
656 );
657 }
658
659 #[test]
660 fn negotiation_uses_exact_tokens_and_local_preference() {
661 use CompressionAlgorithm::{Gzip, Identity, Lz4, Zstd};
662 assert_eq!(negotiate_compression(&[Zstd, Gzip], "gzip, zstd"), Zstd);
663 assert_eq!(negotiate_compression(&[Zstd, Gzip], "other, gzip"), Gzip);
664 for value in ["", "ZSTD", "x-zstd", "*", "zstd;q=1"] {
665 assert_eq!(negotiate_compression(&[Zstd, Gzip], value), Identity);
666 }
667 assert_eq!(
668 accept_encoding_header(&[Zstd, Lz4, Zstd, Identity]),
669 "zstd,lz4,identity"
670 );
671 for algorithm in ALGORITHMS {
672 assert_eq!(
673 algorithm.as_str().parse::<CompressionAlgorithm>().unwrap(),
674 algorithm
675 );
676 }
677 assert_eq!(
678 CompressionAlgorithm::from_encoding("unknown")
679 .unwrap_err()
680 .code(),
681 Code::Unimplemented
682 );
683 }
684}