use std::fs::File;
use std::io::{self, Read};
use std::path::Path;
use crate::sniff::{sniff, Format, SNIFF_LEN};
#[derive(Debug, Default)]
pub struct Outcome {
pub members_seen: usize,
pub members_searched: usize,
pub errors: Vec<String>,
pub matched: bool,
pub notes: Vec<String>,
pub unverified_end: bool,
}
struct CacheGuard {
file: File,
drop_cache: bool,
}
impl Drop for CacheGuard {
fn drop(&mut self) {
crate::resource::finish(&self.file, self.drop_cache);
}
}
struct Guarded<R> {
inner: R,
_guard: CacheGuard,
}
impl<R: Read> Read for Guarded<R> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.inner.read(buf)
}
}
impl<R: Read + Send> Guarded<R> {
fn boxed(inner: R, guard: CacheGuard) -> Box<dyn Read + Send>
where
R: 'static,
{
Box::new(Guarded { inner, _guard: guard })
}
}
pub fn open_decoded(path: &Path) -> anyhow::Result<Box<dyn Read + Send>> {
open_stream(path, false)
}
pub fn open_decoded_dropping(path: &Path, drop_cache: bool) -> anyhow::Result<Box<dyn Read + Send>> {
open_stream(path, drop_cache)
}
fn open_stream(path: &Path, drop_cache: bool) -> anyhow::Result<Box<dyn Read + Send>> {
match open_input(path, drop_cache)? {
Opened::Stream(r, _) => Ok(r),
Opened::Zip(..) => anyhow::bail!("a zip is not a stream; it is read through walk_archive"),
}
}
enum Opened {
Stream(Box<dyn Read + Send>, bool),
Zip(File, CacheGuard),
}
fn open_input(path: &Path, drop_cache: bool) -> anyhow::Result<Opened> {
let f = File::open(path)?;
let dup = f.try_clone()?;
let rdr = io::BufReader::with_capacity(64 * 1024, f);
let (head, body) = peek(rdr)?;
let format = sniff(&head);
let drop_cache = drop_cache && format != Format::Plain;
crate::resource::prepare(&dup, drop_cache);
let guard = CacheGuard { file: dup, drop_cache };
let unverified_end = format == Format::CompressZ;
let stream = match format {
Format::Gzip => Guarded::boxed(flate2::read::MultiGzDecoder::new(body), guard),
Format::Zstd => Guarded::boxed(zstd_decoder(body)?, guard),
Format::Xz => Guarded::boxed(liblzma::read::XzDecoder::new_multi_decoder(body), guard),
Format::Bzip2 => Guarded::boxed(bzip2::read::MultiBzDecoder::new(body), guard),
Format::Lz4 => Guarded::boxed(lz4_decoder(&head, body)?, guard),
Format::CompressZ => Guarded::boxed(compress_z_decoder(body)?, guard),
Format::Zip => {
drop(body);
let mut file = guard.file.try_clone()?;
io::Seek::rewind(&mut file)?;
return Ok(Opened::Zip(file, guard));
}
Format::Tar | Format::Plain => Guarded::boxed(body, guard),
};
Ok(Opened::Stream(stream, unverified_end))
}
fn compress_z_decoder<R: Read + Send + 'static>(r: R) -> io::Result<Box<dyn stuffr_core::Source>> {
use stuffr_core::Codec;
stuffr_formats::legacy::compress_z::CompressZ
.decoder(Box::new(stuffr_core::ReaderSource::new(r)), &Default::default())
.map_err(io::Error::other)
}
pub fn walk_archive<F>(
path: &Path,
drop_cache: bool,
globs: Option<&crate::source::Globs>,
f: F,
) -> Outcome
where
F: FnMut(&str, &mut dyn Read) -> io::Result<()>,
{
let mut out = match open_input(path, drop_cache) {
Ok(Opened::Stream(r, unverified_end)) => Outcome {
unverified_end,
..for_each_member(r, globs, f)
},
Ok(Opened::Zip(file, _guard)) => for_each_zip_member(file, globs, f),
Err(e) => {
let mut o = Outcome::default();
o.errors.push(e.to_string());
o
}
};
for e in &mut out.errors {
*e = format!("{}: {e}", path.display());
}
out
}
fn for_each_zip_member<F>(file: File, globs: Option<&crate::source::Globs>, mut f: F) -> Outcome
where
F: FnMut(&str, &mut dyn Read) -> io::Result<()>,
{
use stuffr_core::{Container, EntryKind};
use stuffr_formats::zip::{Zip, ZIP};
let mut out = Outcome::default();
let opened = stuffr_core::FileSource::from_file(file).and_then(|src| {
let resolved =
stuffr_core::resolve(Box::new(src), ZIP, Zip.caps(), &stuffr_core::StreamPolicy::Exact)?;
Zip.open(resolved, &Default::default())
});
let mut archive = match opened {
Ok(a) => a,
Err(e) => {
out.errors.push(format!("not a readable zip: {e}"));
return out;
}
};
const GIVE_UP_AFTER: usize = 1000;
let mut failed_in_a_row = 0;
let mut index = 0usize;
loop {
index += 1;
let mut entry = match archive.next_entry() {
Ok(Some(e)) => e,
Ok(None) => break,
Err(e) => {
out.errors.push(format!("zip entry {index}: {e}"));
failed_in_a_row += 1;
if failed_in_a_row >= GIVE_UP_AFTER {
out.errors.push(format!("gave up after {GIVE_UP_AFTER} unreadable zip entries in a row"));
break;
}
continue;
}
};
failed_in_a_row = 0;
if !matches!(entry.meta().kind, EntryKind::File) {
continue;
}
let name = entry.meta().name.clone();
out.members_seen += 1;
if globs.is_some_and(|set| !set.is_match(&name)) {
continue;
}
match decode_member(&name, entry.reader(), &mut f) {
Ok(()) => out.members_searched += 1,
Err(e) => out.errors.push(format!("{name}: {e}")),
}
}
let report = archive.fidelity();
if report.has_warnings() {
out.errors.push(format!("{report:?}"));
}
out
}
fn zstd_decoder<'a, R: Read>(r: R) -> io::Result<zstd::stream::read::Decoder<'a, io::BufReader<R>>> {
let mut d = zstd::stream::read::Decoder::new(r)?;
d.window_log_max(31)?;
Ok(d)
}
fn lz4_decoder<R: Read>(head: &[u8], mut r: R) -> io::Result<Lz4Frames<R>> {
let skip = crate::sniff::skippable_prefix(head).unwrap_or(0) as u64;
io::copy(&mut (&mut r).take(skip), &mut io::sink())?;
Ok(Lz4Frames(lz4_flex::frame::FrameDecoder::new(io::BufReader::new(r))))
}
struct Lz4Frames<R: Read>(lz4_flex::frame::FrameDecoder<io::BufReader<R>>);
impl<R: Read> Read for Lz4Frames<R> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
use io::BufRead;
loop {
let n = self.0.read(buf)?;
if n > 0 || buf.is_empty() || self.0.get_mut().fill_buf()?.is_empty() {
return Ok(n);
}
}
}
}
type Peeked<R> = (Vec<u8>, io::Chain<io::Cursor<Vec<u8>>, R>);
fn peek<R: Read>(mut rdr: R) -> io::Result<Peeked<R>> {
let mut head = vec![0u8; SNIFF_LEN];
let mut filled = 0;
while filled < head.len() {
match rdr.read(&mut head[filled..])? {
0 => break,
n => filled += n,
}
}
head.truncate(filled);
let replay = io::Cursor::new(head.clone());
Ok((head, replay.chain(rdr)))
}
fn decode_member<F>(name: &str, member: &mut dyn Read, f: &mut F) -> io::Result<()>
where
F: FnMut(&str, &mut dyn Read) -> io::Result<()>,
{
let (inner_head, mut inner) = peek(member)?;
match sniff(&inner_head) {
Format::Gzip => f(name, &mut flate2::read::MultiGzDecoder::new(&mut inner)),
Format::Zstd => f(name, &mut zstd_decoder(&mut inner)?),
Format::Xz => f(name, &mut liblzma::read::XzDecoder::new_multi_decoder(&mut inner)),
Format::Bzip2 => f(name, &mut bzip2::read::MultiBzDecoder::new(&mut inner)),
Format::Lz4 => f(name, &mut lz4_decoder(&inner_head, &mut inner)?),
Format::CompressZ => Err(io::Error::other(
".Z (compress) data is read only as a whole file, not inside an archive",
)),
_ => f(name, &mut inner),
}
}
pub fn for_each_member<R, F>(rdr: R, globs: Option<&crate::source::Globs>, mut f: F) -> Outcome
where
R: Read,
F: FnMut(&str, &mut dyn Read) -> io::Result<()>,
{
let mut out = Outcome::default();
let (head, mut body) = match peek(rdr) {
Ok(v) => v,
Err(e) => {
out.errors.push(format!("read failed: {e}"));
return out;
}
};
if sniff(&head) != Format::Tar {
out.members_seen = 1;
match f("", &mut body) {
Ok(()) => out.members_searched = 1,
Err(e) => out.errors.push(format!("search failed: {e}")),
}
return out;
}
let mut ar = tar::Archive::new(body);
let entries = match ar.entries() {
Ok(e) => e,
Err(e) => {
out.errors.push(format!("not a readable tar: {e}"));
return out;
}
};
for entry in entries {
let mut entry = match entry {
Ok(e) => e,
Err(e) => {
out.errors.push(format!("tar entry: {e}"));
break;
}
};
if !entry.header().entry_type().is_file() {
continue;
}
let name = match entry.path() {
Ok(p) => p.to_string_lossy().into_owned(),
Err(e) => {
out.errors.push(format!("bad member path: {e}"));
continue;
}
};
out.members_seen += 1;
if globs.is_some_and(|set| !set.is_match(&name)) {
continue;
}
let res = decode_member(&name, &mut entry, &mut f);
match res {
Ok(()) => out.members_searched += 1,
Err(e) => out.errors.push(format!("{name}: {e}")),
}
}
out
}