use std::collections::{HashSet, VecDeque};
use std::fs::File;
use std::io::{self, BufReader, BufWriter, Read, Write};
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::Arc;
use std::thread;
use std::time::Instant;
use anyhow::Result;
use gatling::gatling::ordered::{run_ordered_sink, OrderedSink};
use lgz::chunk::{decode_segment_into_hint, split_chunk};
const CHUNK_SIZE: usize = 200 * 1024 * 1024;
const CARRY_HEADROOM: usize = 32 * 1024 * 1024;
const BUF_CAP: usize = 4 * 1024 * 1024;
struct SegIn {
data: Vec<u8>,
hint: usize,
}
struct WriteSink<W: Write> {
w: W,
total: u64,
}
impl<W: Write> OrderedSink<Vec<u8>> for WriteSink<W> {
fn emit(&mut self, _seq: u64, data: Vec<u8>) -> Result<()> {
if !data.is_empty() {
self.total += data.len() as u64;
self.w.write_all(&data)?;
}
Ok(())
}
}
fn physical_cores() -> usize {
let n = (|| -> Option<usize> {
let mut seen = HashSet::new();
for e in std::fs::read_dir("/sys/devices/system/cpu").ok()? {
let e = e.ok()?;
let s = e.file_name().to_str()?.to_string();
if !s.starts_with("cpu") || s[3..].is_empty()
|| !s[3..].bytes().all(|b| b.is_ascii_digit())
{ continue; }
let pkg = std::fs::read_to_string(e.path().join("topology/physical_package_id")).ok()?;
let core = std::fs::read_to_string(e.path().join("topology/core_id")).ok()?;
seen.insert((pkg.trim().to_string(), core.trim().to_string()));
}
Some(seen.len()).filter(|&n| n > 0)
})();
n.unwrap_or_else(|| thread::available_parallelism().map(|n| n.get()).unwrap_or(4))
}
fn n_workers() -> usize {
std::env::var("LGZ_THREADS")
.ok()
.and_then(|v| v.parse::<usize>().ok())
.filter(|&n| n >= 1)
.unwrap_or_else(physical_cores)
}
fn read_chunk(reader: &mut impl Read, buf: &mut [u8]) -> usize {
let mut got = 0;
while got < buf.len() {
match reader.read(&mut buf[got..]) {
Ok(0) => break,
Ok(k) => got += k,
Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
Err(e) => { eprintln!("lgz: read error: {e}"); break; }
}
}
got
}
fn skip_gzip_header(data: &[u8]) -> Result<usize, &'static str> {
if data.len() < 10 { return Err("too short for gzip header"); }
if data[0] != 0x1f || data[1] != 0x8b { return Err("bad gzip magic"); }
if data[2] != 0x08 { return Err("not DEFLATE"); }
let flags = data[3];
let mut pos = 10;
if flags & 0x04 != 0 {
if pos + 2 > data.len() { return Err("truncated FEXTRA"); }
let xlen = u16::from_le_bytes([data[pos], data[pos + 1]]) as usize;
pos += 2 + xlen;
}
if flags & 0x08 != 0 {
while pos < data.len() && data[pos] != 0 { pos += 1; }
pos += 1;
}
if flags & 0x10 != 0 {
while pos < data.len() && data[pos] != 0 { pos += 1; }
pos += 1;
}
if flags & 0x02 != 0 { pos += 2; }
if pos >= data.len() { return Err("header extends past input"); }
Ok(pos)
}
fn gzip_trailer_ok(raw: &[u8], output: &[u8]) -> bool {
if raw.len() < 8 {
return false;
}
let t = &raw[raw.len() - 8..];
let expected_crc = u32::from_le_bytes([t[0], t[1], t[2], t[3]]);
let expected_isize = u32::from_le_bytes([t[4], t[5], t[6], t[7]]);
let mut crc = flate2::Crc::new();
crc.update(output);
crc.sum() == expected_crc && (output.len() as u32) == expected_isize
}
fn try_multimember_fastpath(args: &[String], t0: Instant, n_workers: usize) -> bool {
if args.len() < 2 || args[1] == "-o" {
return false;
}
let input_path = &args[1];
let in_file = match File::open(input_path) {
Ok(f) => f,
Err(_) => return false,
};
let mmap = match unsafe { memmap2::Mmap::map(&in_file) } {
Ok(m) => m,
Err(_) => return false,
};
let raw: &[u8] = &mmap;
let input_size = raw.len() as u64;
let members = lgz::speculative::find_gzip_members_parallel(raw, n_workers);
if members.len() < 2 {
return false;
}
let ranges: Vec<(usize, usize)> = members
.windows(2)
.map(|w| (w[0], w[1]))
.chain(std::iter::once((*members.last().unwrap(), raw.len())))
.collect();
let decode_member = |s: usize, e: usize, out: &mut Vec<u8>| -> Result<(), String> {
use std::io::Read;
let mut dec = flate2::read::GzDecoder::new(&raw[s..e]);
dec.read_to_end(out).map_err(|err| err.to_string()).map(|_| ())
};
let out_file = args.iter().position(|a| a == "-o").map(|i| args[i + 1].clone());
let n_members = ranges.len();
let total_out: u64;
match out_file {
Some(path) => {
let mut offsets = Vec::with_capacity(n_members);
let mut sizes = Vec::with_capacity(n_members);
let mut acc = 0u64;
for &(_s, e) in &ranges {
if e < 4 {
return false; }
let isz = u32::from_le_bytes([raw[e - 4], raw[e - 3], raw[e - 2], raw[e - 1]]) as u64;
offsets.push(acc);
sizes.push(isz);
acc += isz;
}
total_out = acc;
let f = File::create(&path).unwrap_or_else(|e| {
eprintln!("lgz: cannot create '{path}': {e}");
std::process::exit(1);
});
if let Err(e) = f.set_len(total_out) {
eprintln!("lgz: cannot size output: {e}");
std::process::exit(1);
}
let ranges_ref = &ranges;
let sizes_ref = &sizes;
let stats = gatling::parwrite::write_segments_positional(
&f, n_members, &offsets, n_workers,
|i, buf| {
let (s, e) = ranges_ref[i];
decode_member(s, e, buf)?;
if buf.len() as u64 != sizes_ref[i] {
return Err(format!(
"member {i} decoded {} bytes, ISIZE says {}",
buf.len(), sizes_ref[i],
));
}
Ok(())
},
);
if stats.failed > 0 {
eprintln!("lgz: {} member(s) failed the fast path — falling back", stats.failed);
return false;
}
}
None => {
let hints: Vec<usize> = ranges
.iter()
.map(|&(_s, e)| {
if e >= 4 {
u32::from_le_bytes([raw[e - 4], raw[e - 3], raw[e - 2], raw[e - 1]]) as usize
} else {
0
}
})
.collect();
let max_hint = hints.iter().copied().max().unwrap_or(0);
let mut w = BufWriter::with_capacity(BUF_CAP, io::stdout());
let mut written = 0u64;
let pool: std::sync::Mutex<Vec<Vec<u8>>> = std::sync::Mutex::new(Vec::new());
let ranges_ref = &ranges;
let pool_ref = &pool;
let mut next_idx = 0usize;
let producer = move || -> Option<((), usize)> {
if next_idx < n_members {
let i = next_idx;
next_idx += 1;
Some(((), i))
} else {
None
}
};
let map = |_label: (), i: usize| -> Option<Vec<u8>> {
use std::io::Read;
let (s, e) = ranges_ref[i];
let mut o = pool_ref.lock().unwrap().pop().unwrap_or_default();
o.clear();
o.reserve(max_hint); let mut dec = flate2::read::GzDecoder::new(&raw[s..e]);
dec.read_to_end(&mut o).ok().map(|_| o)
};
let mut sink = |_seq: u64, opt: Option<Vec<u8>>| -> Result<()> {
match opt {
Some(mut d) => {
written += d.len() as u64;
w.write_all(&d)?;
d.clear();
pool_ref.lock().unwrap().push(d); Ok(())
}
None => anyhow::bail!("member decode failed mid-stream"),
}
};
run_ordered_sink(producer, n_workers, 0, map, &mut sink).unwrap_or_else(|e| {
eprintln!("lgz: {e}");
std::process::exit(1);
});
w.flush().expect("flush output");
total_out = written;
}
}
let elapsed = t0.elapsed();
eprintln!(
"lgz: {} → {} bytes ({:.1}×) in {:.3}s ({:.0} MB/s output) [multi-member streaming, {} members]",
input_size, total_out,
total_out as f64 / input_size.max(1) as f64,
elapsed.as_secs_f64(),
total_out as f64 / elapsed.as_secs_f64() / 1_048_576.0,
n_members,
);
lgz::functional_status(
"lgz", "decode", true,
&format!("multi-member gzip decode ({n_members} members) in {:.3}s", elapsed.as_secs_f64()),
);
true
}
fn main() {
let t0 = Instant::now();
let args: Vec<String> = std::env::args().collect();
if try_multimember_fastpath(&args, t0, n_workers()) {
return;
}
let (input, output): (Box<dyn Read + Send>, Box<dyn Write + Send>) = match args.len() {
1 => (Box::new(io::stdin()), Box::new(io::stdout())),
2 => {
let f = File::open(&args[1]).unwrap_or_else(|e| {
eprintln!("lgz: cannot open '{}': {}", args[1], e);
std::process::exit(1);
});
(Box::new(f), Box::new(io::stdout()))
}
_ if args.contains(&"-o".to_string()) => {
let o_idx = args.iter().position(|a| a == "-o").unwrap();
let input_file = &args[1];
let output_file = &args[o_idx + 1];
let f_in = File::open(input_file).unwrap_or_else(|e| {
eprintln!("lgz: cannot open '{}': {}", input_file, e);
std::process::exit(1);
});
let f_out = File::create(output_file).unwrap_or_else(|e| {
eprintln!("lgz: cannot create '{}': {}", output_file, e);
std::process::exit(1);
});
(Box::new(f_in), Box::new(f_out))
}
_ => {
eprintln!("Usage: lgz [input.gz] [-o output]");
std::process::exit(1);
}
};
let n_workers = n_workers();
eprintln!("lgz: {n_workers} gatling workers");
let fallback_flag = Arc::new(AtomicBool::new(false));
let input_size_atomic = Arc::new(AtomicU64::new(0));
let producer = {
let fb = Arc::clone(&fallback_flag);
let isz = Arc::clone(&input_size_atomic);
let mut reader = BufReader::with_capacity(BUF_CAP, input);
let mut carry: Vec<u8> = Vec::new();
let mut chunk_id: u64 = 0;
let mut header_stripped = false;
let mut finished = false;
let mut pending: VecDeque<SegIn> = VecDeque::new();
move || -> Option<((), SegIn)> {
loop {
if let Some(item) = pending.pop_front() {
return Some(((), item));
}
if finished {
return None;
}
let base = carry.len();
let mut data = std::mem::take(&mut carry);
data.resize(base + CHUNK_SIZE, 0);
let read_len = read_chunk(&mut reader, &mut data[base..]);
data.truncate(base + read_len);
let is_last = read_len < CHUNK_SIZE;
isz.fetch_add(read_len as u64, Ordering::Relaxed);
if read_len == 0 && base == 0 {
finished = true;
return None;
}
let carry_len = base;
if carry_len > CARRY_HEADROOM {
fb.store(true, Ordering::Relaxed);
finished = true;
return None;
}
let data_end = data.len();
let deflate_start = if !header_stripped {
header_stripped = true;
match skip_gzip_header(&data) {
Ok(hdr_len) => hdr_len,
Err(e) => {
eprintln!("lgz: {e}");
std::process::exit(1);
}
}
} else {
0
};
let deflate_end = if is_last && data_end > deflate_start + 8 {
data_end - 8
} else {
data_end
};
if deflate_end <= deflate_start {
if is_last {
finished = true;
return None;
}
data.truncate(base);
carry = data;
continue;
}
let dslice = &data[deflate_start..deflate_end];
match split_chunk(dslice, n_workers, is_last) {
Some(split) if split.decode_segments > 1 => {
let seg1_start = split.segment_starts[1];
let seg1_data = &dslice[seg1_start..];
let is_full_flush = seg1_data.len() >= 5
&& seg1_data[0] == 0x00
&& seg1_data[1] == 0x00
&& seg1_data[2] == 0x00
&& seg1_data[3] == 0xFF
&& seg1_data[4] == 0xFF;
if !is_full_flush {
let isize_hint = if is_last && data_end >= 4 {
u32::from_le_bytes([
data[data_end - 4], data[data_end - 3],
data[data_end - 2], data[data_end - 1],
]) as usize
} else {
0
};
carry.clear();
pending.push_back(SegIn { data: dslice.to_vec(), hint: isize_hint });
chunk_id += 1;
if is_last {
finished = true;
}
continue;
}
carry.clear();
if split.consumed < dslice.len() {
carry.extend_from_slice(&dslice[split.consumed..]);
}
let decode_segments = split.decode_segments;
for i in 0..decode_segments {
let start_byte = split.segment_starts[i];
let end_byte = if i + 1 < split.segment_starts.len() {
split.segment_starts[i + 1]
} else {
split.consumed
};
pending.push_back(SegIn {
data: dslice[start_byte..end_byte].to_vec(),
hint: 0,
});
}
chunk_id += 1;
if is_last {
finished = true;
}
continue;
}
Some(_) | None => {
if is_last && chunk_id == 0 {
fb.store(true, Ordering::Relaxed);
finished = true;
return None;
}
carry = dslice.to_vec();
if is_last {
if !carry.is_empty() {
let seg = std::mem::take(&mut carry);
pending.push_back(SegIn { data: seg, hint: 0 });
}
finished = true;
}
continue;
}
}
}
}
};
let map = |_label: (), s: SegIn| -> Vec<u8> {
let mut output = Vec::new();
let _ = decode_segment_into_hint(&s.data, &mut output, s.hint);
output
};
let mut sink = WriteSink {
w: BufWriter::with_capacity(BUF_CAP, output),
total: 0u64,
};
run_ordered_sink(producer, n_workers, 0, map, &mut sink).expect("decode pipeline");
sink.w.flush().expect("flush output");
let fallback_needed = fallback_flag.load(Ordering::Relaxed);
let input_size = input_size_atomic.load(Ordering::Relaxed);
let total_out = sink.total;
if fallback_needed {
eprintln!("lgz: no flush boundaries — trying parallel strategies");
let input_path = if args.len() >= 2 && args[1] != "-o" {
args[1].clone()
} else {
eprintln!("lgz: cannot re-read stdin for fallback");
std::process::exit(1);
};
#[cfg(feature = "timing")]
let t_reread = Instant::now();
let mut raw = Vec::new();
File::open(&input_path).unwrap().read_to_end(&mut raw).unwrap();
#[cfg(feature = "timing")]
eprintln!("[timing] file re-read: {:.1}ms ({:.1} MB)", t_reread.elapsed().as_secs_f64() * 1000.0, raw.len() as f64 / 1_048_576.0);
#[cfg(feature = "timing")]
let t_strat = Instant::now();
let buf = if let Some(decoded) = lgz::speculative::decode_concatenated_members(&raw, n_workers) {
#[cfg(feature = "timing")]
eprintln!("[timing] concatenated members decode: {:.1}ms output={:.1}MB", t_strat.elapsed().as_secs_f64() * 1000.0, decoded.len() as f64 / 1_048_576.0);
decoded
} else {
#[cfg(feature = "timing")]
eprintln!("[timing] no concatenated members ({:.1}ms), trying speculative", t_strat.elapsed().as_secs_f64() * 1000.0);
let deflate_start = skip_gzip_header(&raw).unwrap_or(10);
let deflate_end = if raw.len() > deflate_start + 8 { raw.len() - 8 } else { raw.len() };
let deflate_data = &raw[deflate_start..deflate_end];
#[cfg(feature = "timing")]
let t_spec = Instant::now();
let spec = lgz::speculative::speculative_decode(deflate_data, n_workers)
.filter(|out| gzip_trailer_ok(&raw, out));
if let Some(decoded) = spec {
#[cfg(feature = "timing")]
eprintln!("[timing] speculative decode: {:.1}ms output={:.1}MB", t_spec.elapsed().as_secs_f64() * 1000.0, decoded.len() as f64 / 1_048_576.0);
eprintln!("lgz: speculative parallel decode succeeded (trailer verified)");
decoded
} else {
#[cfg(feature = "timing")]
eprintln!("[timing] speculative failed/unverified ({:.1}ms), flate2 fallback", t_spec.elapsed().as_secs_f64() * 1000.0);
eprintln!("lgz: single-threaded flate2 fallback");
#[cfg(feature = "timing")]
let t_flate = Instant::now();
let mut decoder = flate2::read::GzDecoder::new(raw.as_slice());
let mut buf = Vec::new();
decoder.read_to_end(&mut buf).unwrap_or_else(|e| {
eprintln!("lgz: decompression failed: {e}");
std::process::exit(1);
});
#[cfg(feature = "timing")]
eprintln!("[timing] flate2 decode: {:.1}ms output={:.1}MB", t_flate.elapsed().as_secs_f64() * 1000.0, buf.len() as f64 / 1_048_576.0);
buf
}
};
let output: Box<dyn Write> = if let Some(o_idx) = args.iter().position(|a| a == "-o") {
Box::new(File::create(&args[o_idx + 1]).unwrap())
} else {
Box::new(io::stdout())
};
let mut w = BufWriter::with_capacity(BUF_CAP, output);
w.write_all(&buf).unwrap();
w.flush().unwrap();
let elapsed = t0.elapsed();
eprintln!(
"lgz: {} → {} bytes ({:.1}×) in {:.3}s ({:.0} MB/s output)",
input_size, buf.len(),
buf.len() as f64 / input_size.max(1) as f64,
elapsed.as_secs_f64(),
buf.len() as f64 / elapsed.as_secs_f64() / 1_048_576.0,
);
} else {
let elapsed = t0.elapsed();
eprintln!(
"lgz: {} → {} bytes ({:.1}×) in {:.3}s ({:.0} MB/s output)",
input_size, total_out,
total_out as f64 / input_size.max(1) as f64,
elapsed.as_secs_f64(),
total_out as f64 / elapsed.as_secs_f64() / 1_048_576.0,
);
}
lgz::functional_status(
"lgz",
"decode",
true,
&format!("gzip decode completed in {:.3}s", t0.elapsed().as_secs_f64()),
);
}