1use std::io::{self, Read, Seek, SeekFrom};
18use std::path::Path;
19
20use crate::cli::InflateMode;
21
22const MIN_CAPACITY: u64 = 1 << 16; #[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum Strategy {
29 Stream,
30 Buffer { capacity: usize, ceiling: usize },
31}
32
33fn is_gzip(path: &Path) -> bool {
35 use std::io::Read;
36 let Ok(mut f) = std::fs::File::open(path) else { return false };
37 let mut m = [0u8; 2];
38 f.read_exact(&mut m).is_ok() && m == [0x1f, 0x8b]
39}
40
41pub fn isize_hint(path: &Path) -> Option<u64> {
43 let mut f = std::fs::File::open(path).ok()?;
44 let len = f.metadata().ok()?.len();
45 if len < 18 {
46 return None; }
48 f.seek(SeekFrom::End(-4)).ok()?;
49 let mut b = [0u8; 4];
50 f.read_exact(&mut b).ok()?;
51 Some(u32::from_le_bytes(b) as u64)
52}
53
54fn effective_ceiling(budget: u64) -> u64 {
59 budget.max(MIN_CAPACITY)
60}
61
62pub fn choose(mode: InflateMode, path: &Path, budget: u64) -> Strategy {
63 if !is_gzip(path) {
67 return Strategy::Stream;
68 }
69 match mode {
70 InflateMode::Stream => Strategy::Stream,
71 InflateMode::Buffer => buffer_within_budget(path, budget, isize_hint(path)),
72 InflateMode::Auto => {
73 let Some(hint) = isize_hint(path) else { return Strategy::Stream };
74 let compressed = std::fs::metadata(path).map(|m| m.len()).unwrap_or(u64::MAX);
75
76 if hint < compressed / 2 {
80 return Strategy::Stream;
81 }
82 if hint == 0 || hint > budget {
86 return Strategy::Stream;
87 }
88 buffer_within_budget(path, budget, Some(hint))
89 }
90 }
91}
92
93fn buffer_within_budget(path: &Path, budget: u64, hint: Option<u64>) -> Strategy {
100 let ceiling = effective_ceiling(budget);
101 let compressed = std::fs::metadata(path).map(|m| m.len()).unwrap_or(u64::MAX);
102 if compressed > ceiling {
103 return Strategy::Stream;
104 }
105 let capacity = hint.unwrap_or(1 << 20).min(budget).max(MIN_CAPACITY);
106 Strategy::Buffer { capacity: capacity as usize, ceiling: ceiling as usize }
107}
108
109pub fn inflate_buffered(path: &Path, capacity: usize, ceiling: usize) -> io::Result<Vec<u8>> {
117 let raw = std::fs::read(path)?;
118 inflate_bytes(&raw, path, capacity, ceiling)
119}
120
121pub fn inflate_bytes(
126 raw: &[u8],
127 path: &Path,
128 capacity: usize,
129 ceiling: usize,
130) -> io::Result<Vec<u8>> {
131 let mut cap = capacity.max(MIN_CAPACITY as usize);
132 let ceiling = ceiling.max(cap);
133
134 loop {
135 let mut d = libdeflater::Decompressor::new();
136 let mut out = vec![0u8; cap];
137 match d.gzip_decompress(raw, &mut out) {
138 Ok(n) => {
139 out.truncate(n);
140 return Ok(out);
141 }
142 Err(libdeflater::DecompressionError::InsufficientSpace) if cap < ceiling => {
143 cap = cap.saturating_mul(4).min(ceiling);
144 }
145 Err(libdeflater::DecompressionError::InsufficientSpace) => {
146 return Err(io::Error::other(format!(
151 "the {ceiling}-byte inflate budget would be exceeded"
152 )));
153 }
154 Err(e) => {
155 return Err(io::Error::other(format!(
156 "whole-buffer inflate failed for {}: {e:?}",
157 path.display()
158 )));
159 }
160 }
161 }
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167 use std::io::Write;
168
169 fn gz(body: &[u8]) -> Vec<u8> {
170 let mut e = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
171 e.write_all(body).unwrap();
172 e.finish().unwrap()
173 }
174
175 fn incompressible(n: usize) -> Vec<u8> {
179 let mut s: u64 = 0x2545F4914F6CDD1D;
180 (0..n)
181 .map(|_| {
182 s = s.wrapping_mul(6364136223846793005).wrapping_add(1);
183 (s >> 33) as u8
184 })
185 .collect()
186 }
187
188 fn tmp(bytes: &[u8]) -> (tempfile::TempDir, std::path::PathBuf) {
189 let d = tempfile::tempdir().unwrap();
190 let p = d.path().join("x.gz");
191 std::fs::write(&p, bytes).unwrap();
192 (d, p)
193 }
194
195 #[test]
196 fn reads_the_isize_trailer() {
197 let body = vec![b'a'; 5000];
198 let (_d, p) = tmp(&gz(&body));
199 assert_eq!(isize_hint(&p), Some(5000));
200 }
201
202 #[test]
203 fn isize_of_a_too_short_file_is_none() {
204 let (_d, p) = tmp(b"tiny");
205 assert_eq!(isize_hint(&p), None);
206 }
207
208 #[test]
209 fn auto_uses_buffer_when_the_hint_fits_the_budget() {
210 let (_d, p) = tmp(&gz(&vec![b'a'; 5000]));
211 assert!(matches!(
212 choose(crate::cli::InflateMode::Auto, &p, 64 * 1024),
213 Strategy::Buffer { .. }
214 ));
215 }
216
217 #[test]
218 fn auto_streams_when_the_hint_exceeds_the_budget() {
219 let (_d, p) = tmp(&gz(&vec![b'a'; 200_000]));
220 assert!(matches!(
221 choose(crate::cli::InflateMode::Auto, &p, 1024),
222 Strategy::Stream
223 ));
224 }
225
226 #[test]
227 fn auto_streams_when_the_hint_is_implausible() {
228 let mut bytes = gz(&vec![b'a'; 100_000]);
231 let n = bytes.len();
232 bytes[n - 4..].copy_from_slice(&1u32.to_le_bytes());
233 let (_d, p) = tmp(&bytes);
234 assert!(matches!(
235 choose(crate::cli::InflateMode::Auto, &p, 64 * 1024 * 1024),
236 Strategy::Stream
237 ));
238 }
239
240 #[test]
241 fn explicit_modes_override_the_heuristic() {
242 let (_d, p) = tmp(&gz(&vec![b'a'; 5000]));
243 assert!(matches!(choose(crate::cli::InflateMode::Stream, &p, u64::MAX), Strategy::Stream));
244 assert!(matches!(choose(crate::cli::InflateMode::Buffer, &p, 0), Strategy::Buffer { .. }));
245 }
246
247 #[test]
248 fn buffered_inflate_grows_when_the_hint_was_too_small() {
249 let (_d, p) = tmp(&gz(&vec![b'z'; 200_000]));
254 let out = inflate_buffered(&p, 16, 1 << 20).unwrap();
255 assert_eq!(out.len(), 200_000);
256 }
257
258 #[test]
261 fn buffer_mode_capacity_and_ceiling_never_exceed_the_budget() {
262 let (_d, p) = tmp(&gz(&vec![b'a'; 500_000]));
263 let budget = 10_000u64;
264 match choose(InflateMode::Buffer, &p, budget) {
265 Strategy::Buffer { capacity, ceiling } => {
266 let bound = budget.max(1 << 16);
267 assert!(capacity as u64 <= bound, "capacity {capacity} exceeds budget-derived bound {bound}");
268 assert!(ceiling as u64 <= bound, "ceiling {ceiling} exceeds budget-derived bound {bound}");
269 }
270 Strategy::Stream => {} }
272 }
273
274 #[test]
275 fn a_compressed_file_larger_than_the_budget_selects_stream() {
276 let (_d, p) = tmp(&gz(&incompressible(200_000)));
282 assert!(matches!(choose(InflateMode::Buffer, &p, 1024), Strategy::Stream));
283 }
284
285 #[test]
286 fn inflate_budget_zero_still_means_never_buffer_in_auto() {
287 let (_d, p) = tmp(&gz(&vec![b'a'; 5000]));
288 assert!(matches!(choose(InflateMode::Auto, &p, 0), Strategy::Stream));
289 }
290}