use std::collections::HashSet;
use std::fs::File;
use std::io::{Read, Seek, SeekFrom};
use std::path::PathBuf;
use std::thread;
use gatling::parwrite::{extract_entries_unordered, EntryJob};
use lzip_parallel::central_dir::{read_central_directory_from, EntryLocation};
use lzip_parallel::chunk::decode_segment_into;
use lzip_parallel::entry::ZipError;
const LOCAL_HEADER_SIG: u32 = 0x04034b50;
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 fname = e.file_name();
let s = fname.to_str()?;
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 local_data_offset(file: &mut File, loc: &EntryLocation) -> Result<u64, ZipError> {
file.seek(SeekFrom::Start(loc.local_header_offset))
.map_err(|_| ZipError("seek to local header failed"))?;
let mut hdr = [0u8; 30];
file.read_exact(&mut hdr)
.map_err(|_| ZipError("read local header failed"))?;
if u32::from_le_bytes(hdr[0..4].try_into().unwrap()) != LOCAL_HEADER_SIG {
return Err(ZipError("invalid local file header signature"));
}
let name_len = u16::from_le_bytes(hdr[26..28].try_into().unwrap()) as u64;
let extra_len = u16::from_le_bytes(hdr[28..30].try_into().unwrap()) as u64;
Ok(loc.local_header_offset + 30 + name_len + extra_len)
}
fn decode_entry(comp: &[u8], out_size: usize, out: &mut Vec<u8>) -> Result<(), String> {
let need = out_size + linflate::OVERWRITE_HEADROOM;
if out.len() < need {
out.resize(need, 0);
}
match linflate::inflate_segment(comp, &mut out[..need]) {
Ok(written) => {
out.truncate(written);
Ok(())
}
Err(linflate::InflateError::OutputOverflow) => {
out.clear();
decode_segment_into(comp, out).map_err(|e| e.to_string())
}
Err(e) => Err(e.to_string()),
}
}
fn main() {
let raw: Vec<String> = std::env::args().skip(1).collect();
let mut n_override: Option<usize> = None;
let mut positional: Vec<String> = Vec::new();
let mut it = raw.into_iter();
while let Some(a) = it.next() {
if let Some(v) = a.strip_prefix("-n") {
n_override = if v.is_empty() { it.next().and_then(|s| s.parse().ok()) } else { v.parse().ok() };
} else {
positional.push(a);
}
}
if positional.is_empty() {
eprintln!("usage: lzip [-n<workers>] <file.zip> [output-dir]");
std::process::exit(1);
}
let zip_path = &positional[0];
let out_dir: Option<PathBuf> = positional.get(1).map(PathBuf::from);
let n_workers = n_override.filter(|&n| n > 0).unwrap_or_else(physical_cores);
let mut file = File::open(zip_path).unwrap_or_else(|e| {
eprintln!("error opening {zip_path}: {e}");
std::process::exit(1);
});
let entries = read_central_directory_from(&mut file).unwrap_or_else(|e| {
eprintln!("error: {e}");
std::process::exit(1);
});
if out_dir.is_none() {
let files: Vec<_> = entries.iter().filter(|e| !e.is_directory).collect();
for e in &files {
println!("{:>10} {}", e.uncompressed_size, e.name);
}
println!("{} entries", files.len());
lzip_parallel::functional_status("lzip", "list", true, &format!("{} entries listed", files.len()));
return;
}
let out_dir = out_dir.unwrap();
let mut sorted: Vec<&EntryLocation> = entries.iter().filter(|e| !e.is_directory).collect();
sorted.sort_by_key(|e| e.local_header_offset);
let mut jobs: Vec<EntryJob> = Vec::with_capacity(sorted.len());
for loc in &sorted {
match local_data_offset(&mut file, loc) {
Ok(off) => jobs.push(EntryJob {
data_offset: off,
comp_len: loc.compressed_size as usize,
out_size: loc.uncompressed_size as usize,
is_store: loc.compression_method == 0,
name: loc.name.clone(),
}),
Err(e) => eprintln!("skipping {}: {}", loc.name, e),
}
}
let n_jobs = jobs.len();
eprintln!("lzip: {n_jobs} entries, {n_workers} parallel writers");
let stats = extract_entries_unordered(&file, &jobs, &out_dir, n_workers, decode_entry);
if stats.failed > 0 {
eprintln!("lzip: {} extracted, {} failed", stats.ok, stats.failed);
}
lzip_parallel::functional_status("lzip", "extract", true, &format!("{} entries extracted", stats.ok));
}