1use std::cell::RefCell;
2use std::io;
3use zlib_rs::{Inflate, InflateError, InflateFlush, Status};
4
5const ZLIB_HEADER: bool = true;
7const WINDOW_BITS: u8 = 15;
8
9thread_local! {
10 static DECOMPRESSOR: RefCell<(Inflate, Vec<u8>)> = RefCell::new((
11 Inflate::new(ZLIB_HEADER, WINDOW_BITS),
12 Vec::with_capacity(4096),
13 ));
14
15 static INFLATE_POOL: RefCell<Vec<(Inflate, Vec<u8>)>> = const { RefCell::new(Vec::new()) };
20}
21
22fn inflate_into_spare(
28 inflate: &mut Inflate,
29 input: &[u8],
30 out: &mut Vec<u8>,
31 flush: InflateFlush,
32) -> Result<Status, InflateError> {
33 let before = inflate.total_out();
34 let status = inflate.decompress_uninit(input, out.spare_capacity_mut(), flush)?;
35 let produced = (inflate.total_out() - before) as usize;
36 unsafe { out.set_len(out.len() + produced) };
39 Ok(status)
40}
41
42pub struct InflateReader<'a> {
50 input: &'a [u8],
51 in_pos: usize,
52 decomp: Option<Inflate>,
55 buf: Vec<u8>,
56 cursor: usize,
57 total_out: u64,
58 max: u64,
59 eof: bool,
60 stream_end: bool,
61}
62
63impl<'a> InflateReader<'a> {
64 const CHUNK: usize = 64 * 1024;
66 const RETAINED_CAPACITY: usize = Self::CHUNK;
70 const POOL_MAX: usize = 4;
73
74 pub fn new(input: &'a [u8], max: u64) -> Self {
75 let (decomp, buf) = INFLATE_POOL.with(|p| p.borrow_mut().pop()).map_or_else(
76 || {
77 (
78 Inflate::new(ZLIB_HEADER, WINDOW_BITS),
79 Vec::with_capacity(Self::CHUNK),
80 )
81 },
82 |(mut decomp, mut buf)| {
83 decomp.reset(ZLIB_HEADER);
84 buf.clear();
85 (decomp, buf)
86 },
87 );
88 Self {
89 input,
90 in_pos: 0,
91 decomp: Some(decomp),
92 buf,
93 cursor: 0,
94 total_out: 0,
95 max,
96 eof: false,
97 stream_end: false,
98 }
99 }
100
101 #[inline]
103 pub fn available(&self) -> &[u8] {
104 &self.buf[self.cursor..]
105 }
106
107 #[inline]
109 pub fn consume(&mut self, n: usize) {
110 self.cursor = (self.cursor + n).min(self.buf.len());
111 }
112
113 pub fn ensure(&mut self, need: usize) -> io::Result<bool> {
116 while self.buf.len() - self.cursor < need {
117 if self.eof {
118 return Ok(false);
119 }
120 self.pump()?;
121 }
122 Ok(true)
123 }
124
125 pub fn is_done(&self) -> bool {
127 self.eof && self.cursor >= self.buf.len()
128 }
129
130 pub fn total_out(&self) -> u64 {
133 self.total_out
134 }
135
136 #[inline]
140 pub fn compressed_progress(&self) -> (usize, usize) {
141 (self.in_pos, self.input.len())
142 }
143
144 pub fn stream_ended(&self) -> bool {
148 self.stream_end
149 }
150
151 fn pump(&mut self) -> io::Result<()> {
152 if self.cursor != 0 {
158 let remaining = self.buf.len() - self.cursor;
159 self.buf.copy_within(self.cursor.., 0);
160 self.buf.truncate(remaining);
161 self.cursor = 0;
162 }
163
164 let decomp = self
167 .decomp
168 .as_mut()
169 .ok_or_else(|| io::Error::other("InflateReader used after pool return"))?;
170 if self.buf.len() == self.buf.capacity() {
174 self.buf.reserve(Self::CHUNK);
175 }
176 let prev_in = decomp.total_in();
177 let prev_out = decomp.total_out();
178 let status = inflate_into_spare(
179 decomp,
180 &self.input[self.in_pos..],
181 &mut self.buf,
182 InflateFlush::NoFlush,
183 )
184 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.as_str()))?;
185 let new_in = decomp.total_in();
186 let produced = (decomp.total_out() - prev_out) as usize;
187 self.in_pos += (new_in - prev_in) as usize;
188 self.total_out += produced as u64;
189 if self.total_out > self.max {
190 return Err(io::Error::new(
191 io::ErrorKind::InvalidData,
192 format!("decompressed payload exceeds {} bytes", self.max),
193 ));
194 }
195
196 match status {
197 Status::StreamEnd => {
198 self.eof = true;
199 self.stream_end = true;
200 }
201 _ if produced == 0 => {
209 if self.in_pos >= self.input.len() {
210 self.eof = true;
211 } else if new_in == prev_in {
212 return Err(io::Error::new(
213 io::ErrorKind::InvalidData,
214 "zlib stream stalled (no progress)",
215 ));
216 }
217 }
218 _ => {}
219 }
220 Ok(())
221 }
222}
223
224impl Drop for InflateReader<'_> {
225 fn drop(&mut self) {
226 if let Some(decomp) = self.decomp.take() {
229 let mut buf = std::mem::take(&mut self.buf);
230 buf.clear();
235 if buf.capacity() > Self::RETAINED_CAPACITY {
236 buf.shrink_to(Self::RETAINED_CAPACITY);
237 }
238 INFLATE_POOL.with(|p| {
239 let mut pool = p.borrow_mut();
240 if pool.len() < Self::POOL_MAX {
241 pool.push((decomp, buf));
242 }
243 });
244 }
245 }
246}
247
248fn grow_by_observed_ratio(
255 scratch: &mut Vec<u8>,
256 decompressor: &Inflate,
257 compressed_len: usize,
258 cap: usize,
259) {
260 let consumed = decompressor.total_in() as usize;
261 let produced = decompressor.total_out() as usize;
262 let remaining_in = compressed_len.saturating_sub(consumed) as u64;
263 let projected = if consumed > 0 && produced > 0 {
264 ((produced as u64).saturating_mul(remaining_in) / consumed as u64).saturating_mul(9) / 8
267 } else {
268 0
269 };
270 let min_grow = scratch.capacity().max(4096);
274 let want = (projected.min(usize::MAX as u64) as usize)
275 .max(min_grow)
276 .min(cap - scratch.len());
277 scratch.reserve(want);
278}
279
280pub fn decompress_zlib_pooled(compressed: &[u8], max_size: u64) -> io::Result<Vec<u8>> {
287 DECOMPRESSOR.with(|cell| {
288 let (decompressor, scratch) = &mut *cell.borrow_mut();
289 decompressor.reset(ZLIB_HEADER);
290 scratch.clear();
291
292 let cap = (max_size as usize).saturating_add(1);
295
296 let floor = 4096.min(cap);
306 let estimated = compressed.len().saturating_mul(2).clamp(floor, cap);
307 if scratch.capacity() < estimated {
308 scratch.reserve(estimated - scratch.capacity());
309 }
310
311 let mut input_offset = 0;
312 loop {
313 if scratch.len() >= cap {
315 return Err(io::Error::new(
316 io::ErrorKind::InvalidData,
317 format!("decompressed payload exceeds {max_size} bytes"),
318 ));
319 }
320
321 let prev_in = decompressor.total_in();
322 let prev_out = decompressor.total_out();
323
324 let status = inflate_into_spare(
325 decompressor,
326 &compressed[input_offset..],
327 scratch,
328 InflateFlush::Finish,
329 )
330 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.as_str()))?;
331
332 input_offset = decompressor.total_in() as usize;
333
334 if scratch.len() as u64 > max_size {
335 return Err(io::Error::new(
336 io::ErrorKind::InvalidData,
337 format!("decompressed payload exceeds {max_size} bytes"),
338 ));
339 }
340
341 match status {
342 Status::StreamEnd => break,
343 Status::Ok => {
344 grow_by_observed_ratio(scratch, decompressor, compressed.len(), cap);
345 }
346 Status::BufError => {
347 if decompressor.total_in() == prev_in && decompressor.total_out() == prev_out {
348 return Err(io::Error::new(
349 io::ErrorKind::InvalidData,
350 "zlib stream truncated (no progress)",
351 ));
352 }
353 grow_by_observed_ratio(scratch, decompressor, compressed.len(), cap);
354 }
355 }
356 }
357
358 let result = std::mem::take(scratch);
362 scratch.reserve(4096);
364 Ok(result)
365 })
366}
367
368#[cfg(test)]
369mod tests {
370 use super::*;
371 use flate2::Compression;
372 use flate2::write::ZlibEncoder;
373 use std::io::Write;
374
375 fn zlib(data: &[u8]) -> Vec<u8> {
376 let mut e = ZlibEncoder::new(Vec::new(), Compression::default());
377 e.write_all(data).unwrap();
378 e.finish().unwrap()
379 }
380
381 fn varied(n: usize) -> Vec<u8> {
382 let mut s: u64 = 0x9e37_79b9_7f4a_7c15;
383 (0..n)
384 .map(|_| {
385 s ^= s << 13;
386 s ^= s >> 7;
387 s ^= s << 17;
388 (s >> 24) as u8
389 })
390 .collect()
391 }
392
393 fn stored_zlib(data: &[u8]) -> Vec<u8> {
402 assert!(data.len() <= u16::MAX as usize, "one stored block only");
403 let mut out = vec![0x78, 0x01];
406 let len = data.len() as u16;
407 out.push(0x01);
409 out.extend_from_slice(&len.to_le_bytes());
410 out.extend_from_slice(&(!len).to_le_bytes());
411 out.extend_from_slice(data);
412
413 let (mut a, mut b) = (1u32, 0u32);
414 for &byte in data {
415 a = (a + byte as u32) % 65521;
416 b = (b + a) % 65521;
417 }
418 out.extend_from_slice(&(((b << 16) | a).to_be_bytes()));
419 out
420 }
421
422 #[test]
428 fn pooled_roundtrip_small_input() {
429 let original = varied(1024);
430 let compressed = stored_zlib(&original);
431 assert_eq!(
432 decompress_zlib_pooled(&compressed, 64 * 1024).unwrap(),
433 original
434 );
435 assert_eq!(drain_reader(&compressed, original.len()), original);
436 }
437
438 #[test]
439 #[cfg_attr(miri, ignore)]
440 fn inflate_reader_roundtrip_across_chunks() {
441 let original = varied(200 * 1024);
444 let compressed = zlib(&original);
445 let mut r = InflateReader::new(&compressed, 64 * 1024 * 1024);
446 let mut out = Vec::with_capacity(original.len());
447 while r.ensure(1).unwrap() {
448 let n = r.available().len().min(7);
449 out.extend_from_slice(&r.available()[..n]);
450 r.consume(n);
451 }
452 assert!(r.is_done());
453 assert_eq!(out, original);
454 }
455
456 #[test]
457 #[cfg_attr(miri, ignore)]
458 fn inflate_reader_ensure_larger_than_chunk() {
459 let original: Vec<u8> = (0..150 * 1024).map(|i| (i % 256) as u8).collect();
461 let compressed = zlib(&original);
462 let mut r = InflateReader::new(&compressed, 64 * 1024 * 1024);
463 assert!(r.ensure(150 * 1024).unwrap());
464 assert_eq!(&r.available()[..150 * 1024], &original[..]);
465 }
466
467 #[test]
468 #[cfg_attr(miri, ignore)]
469 fn inflate_reader_keeps_one_window_for_smaller_records() {
470 INFLATE_POOL.with(|p| p.borrow_mut().clear());
471 const RECORD: usize = 30 * 1024;
472 let original = varied(RECORD * 8);
473 let compressed = zlib(&original);
474 let mut r = InflateReader::new(&compressed, 64 * 1024 * 1024);
475
476 for expected in original.chunks(RECORD) {
477 assert!(r.ensure(expected.len()).unwrap());
478 assert_eq!(&r.available()[..expected.len()], expected);
479 r.consume(expected.len());
480 assert!(
481 r.buf.capacity() <= InflateReader::CHUNK,
482 "sub-window records grew the inflate buffer to {} bytes",
483 r.buf.capacity()
484 );
485 }
486 assert!(!r.ensure(1).unwrap());
487 assert!(r.is_done());
488 }
489
490 #[test]
491 #[cfg_attr(miri, ignore)]
492 fn inflate_reader_enforces_max() {
493 let original = vec![0u8; 1024 * 1024];
494 let compressed = zlib(&original);
495 let mut r = InflateReader::new(&compressed, 4096);
496 assert!(r.ensure(1024 * 1024).is_err());
497 }
498
499 #[test]
500 #[cfg_attr(miri, ignore)]
501 fn pooled_high_ratio_stream_roundtrips() {
502 let original: Vec<u8> = (0..4_000_000u32).map(|i| ((i / 1024) % 7) as u8).collect();
505 let compressed = zlib(&original);
506 assert!(
507 compressed.len() < original.len() / 20,
508 "fixture not high-ratio"
509 );
510 let out = decompress_zlib_pooled(&compressed, 64 * 1024 * 1024).unwrap();
511 assert_eq!(out, original);
512 assert!(
515 out.capacity() < original.len() * 2,
516 "capacity {} vs data {}",
517 out.capacity(),
518 original.len()
519 );
520 }
521
522 #[test]
523 #[cfg_attr(miri, ignore)]
524 fn pooled_oneshot_matches_streaming() {
525 let original = varied(100_000);
526 let compressed = zlib(&original);
527 let one_shot = decompress_zlib_pooled(&compressed, 64 * 1024 * 1024).unwrap();
528 assert_eq!(one_shot, original);
529 }
530
531 fn drain_reader(compressed: &[u8], n: usize) -> Vec<u8> {
532 let mut r = InflateReader::new(compressed, 64 * 1024 * 1024);
533 let mut out = Vec::with_capacity(n);
534 while r.ensure(1).unwrap() {
535 let take = r.available().len();
536 out.extend_from_slice(r.available());
537 r.consume(take);
538 }
539 assert!(r.is_done());
540 out
541 }
542
543 #[test]
544 #[cfg_attr(miri, ignore)]
545 fn inflate_reader_reuses_pool_state_correctly() {
546 for n in [10_000usize, 250_000, 1, 80_000] {
549 let original = varied(n);
550 assert_eq!(drain_reader(&zlib(&original), n), original, "size {n}");
551 }
552 }
553
554 #[test]
555 #[cfg_attr(miri, ignore)]
556 fn inflate_reader_reuse_after_error() {
557 {
560 let compressed = zlib(&varied(500_000));
561 let mut r = InflateReader::new(&compressed, 4096);
562 assert!(r.ensure(500_000).is_err());
563 }
564 let original = varied(120_000);
565 assert_eq!(drain_reader(&zlib(&original), 120_000), original);
566 }
567
568 #[test]
569 #[cfg_attr(miri, ignore)]
570 fn drop_shrinks_oversized_buffer_before_pooling() {
571 INFLATE_POOL.with(|p| p.borrow_mut().clear());
575 let big = varied(2 * 1024 * 1024);
576 let compressed = zlib(&big);
577 {
578 let mut r = InflateReader::new(&compressed, 64 * 1024 * 1024);
579 assert!(r.ensure(big.len()).unwrap());
580 assert!(r.buf.capacity() >= big.len(), "buf should grow while alive");
581 }
582 let pooled = INFLATE_POOL.with(|p| p.borrow().last().map(|(_, b)| b.capacity()));
583 assert!(
584 matches!(pooled, Some(cap) if cap <= InflateReader::RETAINED_CAPACITY),
585 "pooled buffer not shrunk: {pooled:?}"
586 );
587 }
588}