use std::fs::File;
use std::io;
use deflate64::InflaterManaged;
use crate::{pread_exact, ZipCoreError};
pub(crate) const DEFAULT_CHECKPOINT_INTERVAL: u64 = 8 * 1024 * 1024;
const MAX_DECODE: u64 = 4 * 1024 * 1024 * 1024;
const MAX_INDEX_BYTES: usize = 512 * 1024 * 1024;
const IO_CHUNK: usize = 128 * 1024;
struct Checkpoint {
output_offset: u64,
input_offset: u64,
blob: Option<Vec<u8>>,
}
pub(crate) struct Deflate64Index {
data_end: u64,
uncompressed_size: u64,
checkpoints: Vec<Checkpoint>,
}
impl Deflate64Index {
pub(crate) fn checkpoint_count(&self) -> usize {
self.checkpoints.len()
}
pub(crate) fn read_at(&self, file: &File, buf: &mut [u8], offset: u64) -> io::Result<usize> {
if offset >= self.uncompressed_size || buf.is_empty() {
return Ok(0);
}
let want = (self.uncompressed_size - offset).min(buf.len() as u64) as usize;
let ci = self
.checkpoints
.partition_point(|c| c.output_offset <= offset);
let cp = &self.checkpoints[ci - 1];
let mut inflater = Box::new(InflaterManaged::new());
let start_out = match &cp.blob {
None => 0,
Some(blob) => {
let Some(pos) = inflater.restore_from_checkpoint(blob) else {
return Err(io::Error::other("deflate64 restore failed")); };
pos.output_bytes_already_returned
}
};
debug_assert_eq!(start_out, cp.output_offset);
let mut raw = RawInflate::new(file, cp.input_offset, self.data_end, inflater);
let mut to_skip = offset - start_out;
let mut scratch = vec![0u8; IO_CHUNK];
while to_skip > 0 {
let chunk = to_skip.min(scratch.len() as u64) as usize;
let n = raw.read(&mut scratch[..chunk])?;
if n == 0 {
return Err(io::Error::other("deflate64 stream truncated")); }
to_skip -= n as u64;
}
let mut filled = 0usize;
while filled < want {
let n = raw.read(&mut buf[filled..want])?;
if n == 0 {
break; }
filled += n;
}
Ok(filled)
}
}
struct RawInflate<'f> {
file: &'f File,
file_pos: u64,
data_end: u64,
inflater: Box<InflaterManaged>,
in_buf: Vec<u8>,
in_start: usize,
in_end: usize,
}
impl<'f> RawInflate<'f> {
fn new(file: &'f File, file_pos: u64, data_end: u64, inflater: Box<InflaterManaged>) -> Self {
Self {
file,
file_pos,
data_end,
inflater,
in_buf: vec![0u8; IO_CHUNK],
in_start: 0,
in_end: 0,
}
}
fn read(&mut self, out: &mut [u8]) -> io::Result<usize> {
if out.is_empty() {
return Ok(0); }
loop {
if self.in_start == self.in_end {
let avail = (self.data_end - self.file_pos).min(self.in_buf.len() as u64) as usize;
if avail > 0 {
pread_exact(self.file, &mut self.in_buf[..avail], self.file_pos)?;
self.file_pos += avail as u64;
self.in_start = 0;
self.in_end = avail;
}
}
let no_more_input = self.in_start == self.in_end;
let res = self
.inflater
.inflate(&self.in_buf[self.in_start..self.in_end], out);
if res.data_error {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"invalid deflate64 stream",
));
}
self.in_start += res.bytes_consumed;
if res.bytes_written > 0 {
return Ok(res.bytes_written);
}
if self.inflater.finished() {
return Ok(0);
}
if no_more_input && res.bytes_consumed == 0 {
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
"unexpected end of deflate64 stream",
));
}
}
}
}
pub(crate) fn build_index(
file: &File,
name: &str,
data_start: u64,
compressed_size: u64,
uncompressed_size: u64,
interval: u64,
) -> Result<Deflate64Index, ZipCoreError> {
build_index_capped(
file,
name,
data_start,
compressed_size,
uncompressed_size,
interval,
Caps::DEFAULT,
)
}
#[derive(Clone, Copy)]
struct Caps {
max_decode: u64,
max_index_bytes: usize,
}
impl Caps {
const DEFAULT: Caps = Caps {
max_decode: MAX_DECODE,
max_index_bytes: MAX_INDEX_BYTES,
};
}
fn build_index_capped(
file: &File,
name: &str,
data_start: u64,
compressed_size: u64,
uncompressed_size: u64,
interval: u64,
caps: Caps,
) -> Result<Deflate64Index, ZipCoreError> {
let interval = interval.max(1);
let data_end = data_start + compressed_size;
let mut checkpoints = vec![Checkpoint {
output_offset: 0,
input_offset: data_start,
blob: None,
}];
let mut last_output_offset = 0u64;
let mut index_bytes = 0usize;
let mut raw = RawInflate::new(file, data_start, data_end, Box::new(InflaterManaged::new()));
let mut out_buf = vec![0u8; IO_CHUNK];
let mut total_out = 0u64;
let mut next_at = interval;
loop {
let n = raw
.read(&mut out_buf)
.map_err(|e| ZipCoreError::Malformed {
entry: name.to_string(),
reason: format!("deflate64 decode failed while indexing: {e}"),
})?;
if n == 0 {
break; }
total_out += n as u64;
if total_out > caps.max_decode {
return Err(ZipCoreError::Malformed {
entry: name.to_string(),
reason: format!(
"decompressed output exceeds the {}-byte cap",
caps.max_decode
),
});
}
if total_out >= next_at {
match raw.inflater.checkpoint() {
Some((blob, pos)) => {
let output_offset = pos.output_bytes_already_returned;
if output_offset > last_output_offset {
index_bytes += blob.len();
if index_bytes > caps.max_index_bytes {
return Err(ZipCoreError::Malformed {
entry: name.to_string(),
reason: format!(
"deflate64 seek index exceeds the {}-byte cap",
caps.max_index_bytes
),
});
}
checkpoints.push(Checkpoint {
output_offset,
input_offset: data_start + pos.input_bytes_to_skip,
blob: Some(blob),
});
last_output_offset = output_offset;
}
next_at = output_offset.max(total_out) + interval;
}
None => {
next_at = total_out + interval;
}
}
}
}
if total_out != uncompressed_size {
return Err(ZipCoreError::Malformed {
entry: name.to_string(),
reason: format!(
"deflate64 decoded {total_out} bytes != entry uncompressed size {uncompressed_size}"
),
});
}
Ok(Deflate64Index {
data_end,
uncompressed_size,
checkpoints,
})
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used)]
use super::*;
use crate::{CompressionMethod, ZipArchive};
use std::io::Read;
const FIXTURE: &[u8] = include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/../tests/data/codecs/seek-deflate64.zip"
));
fn known_content() -> Vec<u8> {
let mut v = Vec::new();
for i in 0..4096u32 {
v.extend_from_slice(
format!(
"{i:08} the quick brown fox jumps over the lazy dog - lorem ipsum dolor sit amet consectetur\n"
)
.as_bytes(),
);
}
v
}
struct Prepared {
_tmp: tempfile::NamedTempFile,
file: File,
data_start: u64,
compressed_size: u64,
uncompressed_size: u64,
oracle: Vec<u8>,
}
fn prepare() -> Prepared {
let tmp = tempfile::NamedTempFile::new().unwrap();
std::fs::write(tmp.path(), FIXTURE).unwrap();
let mut ar = ZipArchive::new(File::open(tmp.path()).unwrap()).unwrap();
let mut entry = ar.by_name("bigfile.txt").unwrap();
assert_eq!(entry.compression(), CompressionMethod::Deflate64);
let data_start = entry.data_start();
let compressed_size = entry.compressed_size();
let uncompressed_size = entry.size();
let mut oracle = Vec::new();
entry.read_to_end(&mut oracle).unwrap();
drop(entry);
drop(ar);
assert_eq!(oracle.len() as u64, uncompressed_size);
assert_eq!(oracle, known_content(), "full decode vs known generator");
let file = File::open(tmp.path()).unwrap();
Prepared {
_tmp: tmp,
file,
data_start,
compressed_size,
uncompressed_size,
oracle,
}
}
#[test]
fn read_at_matches_full_decompress_oracle_across_checkpoints() {
let p = prepare();
let interval = 64 * 1024;
let index = build_index(
&p.file,
"bigfile.txt",
p.data_start,
p.compressed_size,
p.uncompressed_size,
interval,
)
.unwrap();
let n_ckpt = index.checkpoint_count();
assert!(n_ckpt >= 2, "expected multiple checkpoints, got {n_ckpt}");
let second_ckpt = index.checkpoints[1].output_offset;
let mid = p.uncompressed_size / 2;
assert!(
second_ckpt < mid,
"mid offset {mid} must sit past the 2nd checkpoint {second_ckpt}"
);
let cases: [(u64, usize); 5] = [
(0, 100), (mid, 4096), (interval + 123, 5000), (p.uncompressed_size - 1, 1), (p.uncompressed_size - 3000, 4000), ];
for (off, len) in cases {
let mut buf = vec![0u8; len];
let n = index.read_at(&p.file, &mut buf, off).unwrap();
let end = (off as usize + len).min(p.oracle.len());
assert_eq!(
&buf[..n],
&p.oracle[off as usize..end],
"seek mismatch at off={off} len={len}"
);
}
let mut fwd = vec![0u8; 2000];
let nf = index
.read_at(&p.file, &mut fwd, p.uncompressed_size - 3000)
.unwrap();
assert_eq!(
&fwd[..nf],
&p.oracle
[(p.uncompressed_size - 3000) as usize..(p.uncompressed_size - 3000) as usize + nf]
);
let mut back = vec![0u8; 2000];
let nb = index.read_at(&p.file, &mut back, 500).unwrap();
assert_eq!(&back[..nb], &p.oracle[500..500 + nb]);
}
#[test]
fn read_at_past_end_and_empty_buf_return_zero() {
let p = prepare();
let index = build_index(
&p.file,
"bigfile.txt",
p.data_start,
p.compressed_size,
p.uncompressed_size,
64 * 1024,
)
.unwrap();
let mut buf = [0u8; 8];
assert_eq!(
index
.read_at(&p.file, &mut buf, p.uncompressed_size)
.unwrap(),
0
);
assert_eq!(index.read_at(&p.file, &mut [], 0).unwrap(), 0);
}
fn temp_with(bytes: &[u8]) -> (tempfile::NamedTempFile, File) {
let tmp = tempfile::NamedTempFile::new().unwrap();
std::fs::write(tmp.path(), bytes).unwrap();
let file = File::open(tmp.path()).unwrap();
(tmp, file)
}
#[test]
fn build_index_rejects_corrupt_stream() {
let (_t, file) = temp_with(&[0xFFu8; 64]);
let err = build_index_capped(&file, "x", 0, 64, 1000, 4096, Caps::DEFAULT);
assert!(matches!(err, Err(ZipCoreError::Malformed { .. })));
}
#[test]
fn build_index_rejects_truncated_stream() {
let comp = [&[0x00u8, 0x05, 0x00, 0xFA, 0xFF][..], b"hello"].concat();
let (_t, file) = temp_with(&comp);
let err = build_index_capped(&file, "x", 0, comp.len() as u64, 1000, 4096, Caps::DEFAULT);
assert!(matches!(err, Err(ZipCoreError::Malformed { .. })));
}
#[test]
fn build_index_rejects_size_mismatch() {
let p = prepare();
let err = build_index_capped(
&p.file,
"bigfile.txt",
p.data_start,
p.compressed_size,
p.uncompressed_size + 1,
64 * 1024,
Caps::DEFAULT,
);
assert!(matches!(err, Err(ZipCoreError::Malformed { .. })));
}
#[test]
fn build_index_enforces_decode_bomb_cap() {
let p = prepare();
let err = build_index_capped(
&p.file,
"bigfile.txt",
p.data_start,
p.compressed_size,
p.uncompressed_size,
64 * 1024,
Caps {
max_decode: 10,
max_index_bytes: MAX_INDEX_BYTES,
},
);
assert!(matches!(err, Err(ZipCoreError::Malformed { .. })));
}
#[test]
fn build_index_enforces_index_memory_cap() {
let p = prepare();
let err = build_index_capped(
&p.file,
"bigfile.txt",
p.data_start,
p.compressed_size,
p.uncompressed_size,
64 * 1024,
Caps {
max_decode: MAX_DECODE,
max_index_bytes: 1,
},
);
assert!(matches!(err, Err(ZipCoreError::Malformed { .. })));
}
}