1use crate::codec::Payload;
11use crate::limits::{DEFAULT_COMPRESSION_THRESHOLD_BYTES, DEFAULT_ZSTD_LEVEL, MAX_SAFE_INTEGER};
12use crate::observe::CompressionOutcome;
13
14use super::frame::ProtocolError;
15use super::text::replacement_utf8;
16
17pub const MARKER_ESCAPED_RAW: u8 = 0x00;
20pub const MARKER_ZSTD_UTF8: u8 = 0x01;
22pub const MARKER_ZSTD_BINARY: u8 = 0x02;
24
25const MIN_ZSTD_LEVEL: i32 = 1;
26const MAX_ZSTD_LEVEL: i32 = 22;
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub struct CompressionConfig {
31 pub threshold_bytes: usize,
33 pub level: i32,
35}
36
37impl Default for CompressionConfig {
38 fn default() -> Self {
39 CompressionConfig {
40 threshold_bytes: DEFAULT_COMPRESSION_THRESHOLD_BYTES,
41 level: DEFAULT_ZSTD_LEVEL,
42 }
43 }
44}
45
46impl CompressionConfig {
47 pub fn validate(&self) -> Result<(), ProtocolError> {
49 if self.threshold_bytes < 1 || self.threshold_bytes as u64 > MAX_SAFE_INTEGER {
50 return Err(ProtocolError::Compression(
51 "threshold_bytes must be a positive safe integer".to_string(),
52 ));
53 }
54 if !(MIN_ZSTD_LEVEL..=MAX_ZSTD_LEVEL).contains(&self.level) {
55 return Err(ProtocolError::Compression(
56 "level must be an integer between 1 and 22".to_string(),
57 ));
58 }
59 Ok(())
60 }
61}
62
63#[derive(Debug, Clone, PartialEq, Eq)]
65pub struct CompressionWriteResult {
66 pub payload: Payload,
68 pub outcome: CompressionOutcome,
71 pub original_bytes: usize,
73 pub stored_bytes: usize,
75}
76
77#[derive(Debug, Clone, PartialEq, Eq)]
79pub struct CompressionReadResult {
80 pub payload: Payload,
84 pub outcome: Option<CompressionOutcome>,
87}
88
89fn needs_raw_escape(payload: &Payload) -> bool {
90 payload.binary
91 && payload
92 .bytes
93 .first()
94 .is_some_and(|first| *first <= MARKER_ZSTD_BINARY)
95}
96
97pub fn escape_raw_payload(payload: Payload) -> Payload {
99 if needs_raw_escape(&payload) {
100 let mut escaped = Vec::with_capacity(payload.bytes.len() + 1);
101 escaped.push(MARKER_ESCAPED_RAW);
102 escaped.extend_from_slice(&payload.bytes);
103 return Payload::binary(escaped);
104 }
105 payload
106}
107
108pub fn compress_payload(
117 payload: Payload,
118 config: &CompressionConfig,
119 max_decompressed_bytes: usize,
120) -> Result<CompressionWriteResult, ProtocolError> {
121 config.validate()?;
122 let payload = if payload.binary {
123 payload
124 } else {
125 Payload {
126 bytes: replacement_utf8(&payload.bytes),
127 binary: false,
128 }
129 };
130 let original_bytes = payload.len();
131 let outcome = if original_bytes < config.threshold_bytes {
132 CompressionOutcome::BelowThreshold
133 } else if original_bytes > max_decompressed_bytes {
134 CompressionOutcome::WriteOverLimit
135 } else {
136 let compressed = zstd_compress(&payload.bytes, config.level)?;
137 let raw_bytes = original_bytes + usize::from(needs_raw_escape(&payload));
138 if compressed.len() + 1 < raw_bytes {
139 let mut marked = Vec::with_capacity(compressed.len() + 1);
140 marked.push(if payload.binary {
141 MARKER_ZSTD_BINARY
142 } else {
143 MARKER_ZSTD_UTF8
144 });
145 marked.extend_from_slice(&compressed);
146 return Ok(CompressionWriteResult {
147 stored_bytes: marked.len(),
148 payload: Payload::binary(marked),
149 outcome: CompressionOutcome::Compressed,
150 original_bytes,
151 });
152 }
153 CompressionOutcome::NotSmaller
154 };
155 let raw = escape_raw_payload(payload);
156 Ok(CompressionWriteResult {
157 stored_bytes: raw.len(),
158 payload: raw,
159 outcome,
160 original_bytes,
161 })
162}
163
164pub fn decompress_payload(
177 payload: Payload,
178 max_decompressed_bytes: usize,
179) -> CompressionReadResult {
180 let passthrough = |payload| CompressionReadResult {
181 payload,
182 outcome: None,
183 };
184 if !payload.binary {
185 return passthrough(payload);
186 }
187 let Some(&marker) = payload.bytes.first() else {
188 return passthrough(payload);
189 };
190 match marker {
191 MARKER_ESCAPED_RAW => {
192 if payload
193 .bytes
194 .get(1)
195 .is_some_and(|next| *next <= MARKER_ZSTD_BINARY)
196 {
197 return passthrough(Payload::binary(&payload.bytes[1..]));
198 }
199 passthrough(payload)
200 }
201 MARKER_ZSTD_UTF8 | MARKER_ZSTD_BINARY => {
202 match decode_first_zstd_frame(&payload.bytes[1..], max_decompressed_bytes) {
203 Ok(decoded) => {
204 let decoded = if marker == MARKER_ZSTD_UTF8 {
205 Payload {
206 bytes: replacement_utf8(&decoded),
207 binary: false,
208 }
209 } else {
210 Payload::binary(decoded)
211 };
212 CompressionReadResult {
213 payload: decoded,
214 outcome: Some(CompressionOutcome::Decompressed),
215 }
216 }
217 Err(outcome) => CompressionReadResult {
218 payload,
219 outcome: Some(outcome),
220 },
221 }
222 }
223 _ => passthrough(payload),
224 }
225}
226
227fn zstd_compress(bytes: &[u8], level: i32) -> Result<Vec<u8>, ProtocolError> {
230 let codec =
231 |code: usize| ProtocolError::Compression(zstd_safe::get_error_name(code).to_string());
232 let mut context = zstd_safe::CCtx::create();
233 context
234 .set_parameter(zstd_safe::CParameter::CompressionLevel(level))
235 .map_err(codec)?;
236 context
237 .set_parameter(zstd_safe::CParameter::ChecksumFlag(false))
238 .map_err(codec)?;
239 let mut out = vec![0u8; zstd_safe::compress_bound(bytes.len())];
240 let written = context.compress2(&mut out[..], bytes).map_err(codec)?;
241 out.truncate(written);
242 Ok(out)
243}
244
245fn decode_first_zstd_frame(
249 body: &[u8],
250 max_decompressed_bytes: usize,
251) -> Result<Vec<u8>, CompressionOutcome> {
252 let frame_len =
255 zstd_safe::find_frame_compressed_size(body).map_err(|_| CompressionOutcome::FallbackRaw)?;
256 let frame = body
257 .get(..frame_len)
258 .ok_or(CompressionOutcome::FallbackRaw)?;
259
260 let mut context = zstd_safe::DCtx::create();
261 let mut out = Vec::new();
262 let mut input = zstd_safe::InBuffer::around(frame);
263 let chunk_len = zstd_safe::DCtx::out_size().max(1);
266 let mut chunk = vec![0u8; chunk_len];
267 loop {
268 let mut output = zstd_safe::OutBuffer::around(&mut chunk[..]);
269 let consumed_before = input.pos();
270 let hint = context
271 .decompress_stream(&mut output, &mut input)
272 .map_err(classify_decoder_error)?;
273 let produced = output.as_slice();
274 if out.len() + produced.len() > max_decompressed_bytes {
275 return Err(CompressionOutcome::ReadOverLimit);
276 }
277 out.extend_from_slice(produced);
278 if hint == 0 {
279 return Ok(out);
280 }
281 if input.pos() == frame.len() && produced.is_empty() {
282 return Err(CompressionOutcome::FallbackRaw);
284 }
285 if input.pos() == consumed_before && produced.is_empty() {
286 return Err(CompressionOutcome::FallbackRaw);
287 }
288 }
289}
290
291fn classify_decoder_error(code: usize) -> CompressionOutcome {
294 let name = zstd_safe::get_error_name(code);
295 if name.contains("too much memory") || name.contains("Destination buffer is too small") {
296 CompressionOutcome::ReadOverLimit
297 } else {
298 CompressionOutcome::FallbackRaw
299 }
300}
301
302#[cfg(test)]
303mod tests {
304 use super::*;
305 use crate::limits::MAX_DECOMPRESSED_BYTES;
306
307 fn text(len: usize) -> Payload {
308 Payload::text("x".repeat(len))
309 }
310
311 fn compressed(payload: Payload) -> CompressionWriteResult {
312 let result = compress_payload(
313 payload,
314 &CompressionConfig {
315 threshold_bytes: 1,
316 level: 3,
317 },
318 MAX_DECOMPRESSED_BYTES,
319 )
320 .unwrap();
321 assert_eq!(result.outcome, CompressionOutcome::Compressed);
322 result
323 }
324
325 #[test]
326 fn config_validation() {
327 assert_eq!(CompressionConfig::default().validate(), Ok(()));
328 assert!(CompressionConfig {
329 threshold_bytes: 0,
330 level: 3
331 }
332 .validate()
333 .is_err());
334 assert!(CompressionConfig {
335 threshold_bytes: usize::MAX,
336 level: 3
337 }
338 .validate()
339 .is_err());
340 assert!(CompressionConfig {
341 threshold_bytes: 1,
342 level: 0
343 }
344 .validate()
345 .is_err());
346 assert!(CompressionConfig {
347 threshold_bytes: 1,
348 level: 23
349 }
350 .validate()
351 .is_err());
352 assert_eq!(
353 CompressionConfig {
354 threshold_bytes: 1,
355 level: 22
356 }
357 .validate(),
358 Ok(())
359 );
360 let invalid = compress_payload(
361 text(1),
362 &CompressionConfig {
363 threshold_bytes: 1,
364 level: 23,
365 },
366 MAX_DECOMPRESSED_BYTES,
367 );
368 assert!(matches!(invalid, Err(ProtocolError::Compression(_))));
369 }
370
371 #[test]
372 fn escaping_and_passthrough() {
373 assert_eq!(
374 escape_raw_payload(Payload::binary(vec![0, 9])).bytes,
375 [0, 0, 9]
376 );
377 assert_eq!(escape_raw_payload(Payload::binary(vec![2])).bytes, [0, 2]);
378 assert_eq!(escape_raw_payload(Payload::binary(vec![3])).bytes, [3]);
379 assert_eq!(
380 escape_raw_payload(Payload::binary(Vec::new())).bytes,
381 Vec::<u8>::new()
382 );
383 assert_eq!(escape_raw_payload(Payload::text("\u{0}")).bytes, [0]);
384 assert_eq!(
386 decompress_payload(Payload::binary(vec![0, 1, 7]), 10),
387 CompressionReadResult {
388 payload: Payload::binary(vec![1, 7]),
389 outcome: None
390 }
391 );
392 for untouched in [
393 Payload::binary(vec![0, 3]),
394 Payload::binary(vec![0]),
395 Payload::binary(vec![3, 1]),
396 Payload::binary(Vec::new()),
397 Payload::text("\u{1}x"),
398 ] {
399 assert_eq!(
400 decompress_payload(untouched.clone(), 10),
401 CompressionReadResult {
402 payload: untouched,
403 outcome: None
404 }
405 );
406 }
407 }
408
409 #[test]
411 fn compression_limits_and_independent_loads() {
412 let raw = text(4096);
413 let written = compressed(raw.clone());
414 let too_large = decompress_payload(written.payload.clone(), 4095);
415 assert_eq!(too_large.outcome, Some(CompressionOutcome::ReadOverLimit));
416 assert_eq!(too_large.payload, written.payload);
417 let exact = decompress_payload(written.payload.clone(), 4096);
418 assert_eq!(
419 exact,
420 CompressionReadResult {
421 payload: raw.clone(),
422 outcome: Some(CompressionOutcome::Decompressed)
423 }
424 );
425 let refused = compress_payload(
426 raw.clone(),
427 &CompressionConfig {
428 threshold_bytes: 1,
429 level: 3,
430 },
431 4095,
432 )
433 .unwrap();
434 assert_eq!(refused.outcome, CompressionOutcome::WriteOverLimit);
435 assert_eq!(refused.payload, raw);
436 assert_eq!((refused.original_bytes, refused.stored_bytes), (4096, 4096));
437 let mut first = decompress_payload(written.payload.clone(), MAX_DECOMPRESSED_BYTES);
439 first.payload.bytes[0] = b'y';
440 assert_eq!(
441 decompress_payload(written.payload.clone(), MAX_DECOMPRESSED_BYTES).payload,
442 raw
443 );
444 let escaped = escape_raw_payload(Payload::binary(vec![1, 2, 3]));
445 let mut first = decompress_payload(escaped.clone(), MAX_DECOMPRESSED_BYTES);
446 first.payload.bytes[0] = 9;
447 assert_eq!(
448 decompress_payload(escaped, MAX_DECOMPRESSED_BYTES)
449 .payload
450 .bytes,
451 [1, 2, 3]
452 );
453 assert_eq!(
455 decompress_payload(written.payload.clone(), 0).outcome,
456 Some(CompressionOutcome::ReadOverLimit)
457 );
458 }
459
460 #[test]
462 fn first_frame_and_malformed_bodies() {
463 let source = Payload::text("first".repeat(100));
464 let written = compressed(source.clone());
465 for trailer in [&b"CRC!"[..], &written.payload.bytes[1..]] {
466 let mut marked = written.payload.bytes.clone();
467 marked.extend_from_slice(trailer);
468 let result = decompress_payload(Payload::binary(marked), MAX_DECOMPRESSED_BYTES);
469 assert_eq!(
470 result,
471 CompressionReadResult {
472 payload: source.clone(),
473 outcome: Some(CompressionOutcome::Decompressed)
474 }
475 );
476 }
477 let half = written.payload.bytes[..written.payload.bytes.len() / 2].to_vec();
478 for marked in [
479 vec![1],
480 vec![2],
481 half,
482 vec![1, 0x28, 0xB5, 0x2F, 0xFD],
483 vec![2, 0xFF, 0xFF],
484 ] {
485 let got = decompress_payload(Payload::binary(marked.clone()), MAX_DECOMPRESSED_BYTES);
486 assert_eq!(
487 got,
488 CompressionReadResult {
489 payload: Payload::binary(marked),
490 outcome: Some(CompressionOutcome::FallbackRaw)
491 }
492 );
493 }
494 }
495
496 #[test]
497 fn skippable_frames_decode_to_empty_output() {
498 let skippable = [
500 0x01, 0x50, 0x2A, 0x4D, 0x18, 0x02, 0x00, 0x00, 0x00, 0xAA, 0xBB, 0xCC,
501 ];
502 let got = decompress_payload(Payload::binary(skippable.to_vec()), MAX_DECOMPRESSED_BYTES);
503 assert_eq!(
504 got,
505 CompressionReadResult {
506 payload: Payload::text(""),
507 outcome: Some(CompressionOutcome::Decompressed)
508 }
509 );
510 assert_eq!(
512 decompress_payload(Payload::binary(skippable.to_vec()), 0).outcome,
513 Some(CompressionOutcome::Decompressed)
514 );
515 let truncated = skippable[..8].to_vec();
516 assert_eq!(
517 decompress_payload(Payload::binary(truncated), MAX_DECOMPRESSED_BYTES).outcome,
518 Some(CompressionOutcome::FallbackRaw)
519 );
520 }
521
522 #[test]
523 fn selection_rule_and_types() {
524 let config = CompressionConfig {
525 threshold_bytes: 4,
526 level: 3,
527 };
528 let below = compress_payload(text(3), &config, MAX_DECOMPRESSED_BYTES).unwrap();
529 assert_eq!(
530 (below.outcome, below.original_bytes, below.stored_bytes),
531 (CompressionOutcome::BelowThreshold, 3, 3)
532 );
533 let incompressible = compress_payload(
534 Payload::binary(vec![1, 2, 3, 4]),
535 &config,
536 MAX_DECOMPRESSED_BYTES,
537 )
538 .unwrap();
539 assert_eq!(incompressible.outcome, CompressionOutcome::NotSmaller);
540 assert_eq!(incompressible.payload.bytes, [0, 1, 2, 3, 4]);
541 assert_eq!(
542 (incompressible.original_bytes, incompressible.stored_bytes),
543 (4, 5)
544 );
545 let binary = compressed(Payload::binary(vec![7; 500]));
546 assert_eq!(binary.payload.bytes[0], MARKER_ZSTD_BINARY);
547 assert!(binary.payload.binary && binary.stored_bytes < 500);
548 assert_eq!(
549 decompress_payload(binary.payload, MAX_DECOMPRESSED_BYTES).payload,
550 Payload::binary(vec![7; 500])
551 );
552 let text_result = compressed(text(500));
553 assert_eq!(text_result.payload.bytes[0], MARKER_ZSTD_UTF8);
554 assert_eq!(
555 decompress_payload(text_result.payload, MAX_DECOMPRESSED_BYTES).payload,
556 text(500)
557 );
558 let ill_formed = Payload {
560 bytes: vec![0xE2, 0x82, b'A'],
561 binary: false,
562 };
563 let stored = compress_payload(
564 ill_formed,
565 &CompressionConfig {
566 threshold_bytes: 100,
567 level: 3,
568 },
569 MAX_DECOMPRESSED_BYTES,
570 )
571 .unwrap();
572 assert_eq!(stored.payload, Payload::text("\u{FFFD}A"));
573 assert_eq!((stored.original_bytes, stored.stored_bytes), (4, 4));
574 let mut marked_text =
576 compressed(Payload::binary([0xE2, 0x82, b'A', 0xFF].repeat(50))).payload;
577 marked_text.bytes[0] = MARKER_ZSTD_UTF8;
578 let decoded = decompress_payload(marked_text, MAX_DECOMPRESSED_BYTES);
579 assert_eq!(
580 decoded.payload,
581 Payload::text("\u{FFFD}A\u{FFFD}".repeat(50))
582 );
583 }
584}