#[cfg(test)]
pub mod fixtures;
mod index;
#[cfg(test)]
mod tests;
pub use index::{Entry, EntryKind, Index};
use std::fs::{self, File};
use std::io::{self, BufReader, Read};
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use std::time::SystemTime;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Kind {
Zip,
Tar,
TarGz,
TarZst,
TarXz,
TarBz2,
SevenZ,
}
pub fn kind_of(path: &Path) -> Option<Kind> {
let name = path.file_name()?.to_string_lossy().to_lowercase();
let named = |endings: &[&str]| endings.iter().any(|ending| name.ends_with(ending));
if named(&[".zip"]) {
Some(Kind::Zip)
} else if named(&[".7z"]) {
Some(Kind::SevenZ)
} else if named(&[".tar.gz", ".tgz"]) {
Some(Kind::TarGz)
} else if named(&[".tar.zst", ".tzst"]) {
Some(Kind::TarZst)
} else if named(&[".tar.xz", ".txz"]) {
Some(Kind::TarXz)
} else if named(&[".tar.bz2", ".tbz2", ".tbz"]) {
Some(Kind::TarBz2)
} else if named(&[".tar"]) {
Some(Kind::Tar)
} else {
None
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Located {
pub archive: PathBuf,
pub inner: PathBuf,
}
pub fn locate(path: &Path) -> Option<Located> {
let mut inner = Vec::new();
let mut here = path;
loop {
match fs::metadata(here) {
Ok(metadata) if metadata.is_file() && kind_of(here).is_some() => {
return Some(Located {
archive: here.to_path_buf(),
inner: inner.iter().rev().collect(),
});
}
Ok(_) => return None,
Err(_) => {}
}
inner.push(here.file_name()?.to_os_string());
here = here.parent()?;
}
}
const CACHED: usize = 4;
type Key = (PathBuf, Option<SystemTime>, u64);
static CACHE: Mutex<Vec<(Key, Arc<Index>)>> = Mutex::new(Vec::new());
pub fn index(archive: &Path) -> io::Result<Arc<Index>> {
let kind = kind_of(archive).ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, t!("Not an archive: {path}", path = archive.display())))?;
let metadata = fs::metadata(archive)?;
let key = (archive.to_path_buf(), metadata.modified().ok(), metadata.len());
if let Some((_, index)) = CACHE.lock().unwrap_or_else(std::sync::PoisonError::into_inner).iter().find(|(cached, _)| *cached == key) {
return Ok(Arc::clone(index));
}
let index = Arc::new(Index::read(archive, kind)?);
let mut cache = CACHE.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
cache.retain(|(cached, _)| cached.0 != key.0);
cache.push((key, Arc::clone(&index)));
if cache.len() > CACHED {
cache.remove(0);
}
Ok(index)
}
pub fn entry_at(path: &Path) -> Option<(Arc<Index>, usize)> {
let located = locate(path)?;
let index = index(&located.archive).ok()?;
let at = index.find(&located.inner)?;
Some((index, at))
}
pub fn link_target(bytes: &[u8]) -> PathBuf {
index::path_from_bytes(bytes)
}
fn open(archive: &Path) -> io::Result<BufReader<File>> {
Ok(BufReader::new(File::open(archive)?))
}
pub(crate) fn zip_error(error: zip::result::ZipError) -> io::Error {
match error {
zip::result::ZipError::Io(error) => error,
zip::result::ZipError::UnsupportedArchive(zip::result::ZipError::PASSWORD_REQUIRED) => encrypted(),
other => io::Error::new(io::ErrorKind::InvalidData, other.to_string()),
}
}
pub enum Next {
Go,
Again,
Stop,
}
fn encrypted() -> io::Error {
io::Error::new(io::ErrorKind::Unsupported, t!("Encrypted: fm84 cannot read it without the password"))
}
pub(crate) fn sevenz_error(error: sevenz_rust2::Error) -> io::Error {
match error {
sevenz_rust2::Error::Io(error, _) | sevenz_rust2::Error::FileOpen(error, _) => error,
sevenz_rust2::Error::PasswordRequired | sevenz_rust2::Error::MaybeBadPassword(_) => encrypted(),
sevenz_rust2::Error::UnsupportedCompressionMethod(method) if method.starts_with("AES") => encrypted(),
other => io::Error::new(io::ErrorKind::InvalidData, other.to_string()),
}
}
pub fn read_entries(archive: &Path, kind: Kind, positions: &[usize], visit: &mut dyn FnMut(usize, io::Result<&mut dyn Read>) -> Next) -> io::Result<()> {
match kind {
Kind::Zip => {
let mut zip = zip::ZipArchive::new(open(archive)?).map_err(zip_error)?;
for &position in positions {
loop {
let next = match zip.by_index(position) {
Ok(mut file) => visit(position, Ok(&mut file)),
Err(error) => visit(position, Err(zip_error(error))),
};
match next {
Next::Go => break,
Next::Again => {}
Next::Stop => return Ok(()),
}
}
}
Ok(())
}
Kind::SevenZ => read_7z_entries(archive, positions, visit),
_ => {
let mut pending: std::collections::BTreeSet<usize> = positions.iter().copied().collect();
while !pending.is_empty() {
let mut again = std::collections::BTreeSet::new();
let mut tar = tar::Archive::new(tar_reader(archive, kind)?);
for (ordinal, entry) in tar.entries()?.enumerate() {
if !pending.contains(&ordinal) {
continue;
}
let next = match entry {
Ok(mut entry) => visit(ordinal, Ok(&mut entry)),
Err(error) => {
visit(ordinal, Err(error));
return Ok(());
}
};
match next {
Next::Go => {}
Next::Again => {
again.insert(ordinal);
}
Next::Stop => return Ok(()),
}
pending.remove(&ordinal);
if pending.is_empty() {
break;
}
}
if !pending.is_empty() {
for ordinal in std::mem::take(&mut pending) {
let missing = io::Error::new(io::ErrorKind::UnexpectedEof, t!("The archive ends before it"));
if let Next::Stop = visit(ordinal, Err(missing)) {
return Ok(());
}
}
}
pending = again;
}
Ok(())
}
}
}
fn read_7z_entries(archive: &Path, positions: &[usize], visit: &mut dyn FnMut(usize, io::Result<&mut dyn Read>) -> Next) -> io::Result<()> {
let mut pending: std::collections::BTreeSet<usize> = positions.iter().copied().collect();
while !pending.is_empty() {
let mut again = std::collections::BTreeSet::new();
let mut reader = sevenz_rust2::ArchiveReader::new(open(archive)?, sevenz_rust2::Password::empty()).map_err(sevenz_error)?;
let (base, count) = (reader.archive().files.as_ptr() as usize, reader.archive().files.len());
let mut stopped = false;
let walked = reader.for_each_entries(|entry, contents| {
let position = (entry as *const sevenz_rust2::ArchiveEntry as usize).wrapping_sub(base) / std::mem::size_of::<sevenz_rust2::ArchiveEntry>();
if position < count && pending.remove(&position) {
match visit(position, Ok(&mut *contents)) {
Next::Go => {}
Next::Again => {
again.insert(position);
}
Next::Stop => {
stopped = true;
return Ok(false);
}
}
}
io::copy(contents, &mut io::sink())?;
Ok(!pending.is_empty())
});
if stopped {
return Ok(());
}
if let Err(error) = walked {
let error = sevenz_error(error);
for position in std::mem::take(&mut pending) {
if let Next::Stop = visit(position, Err(io::Error::new(error.kind(), error.to_string()))) {
return Ok(());
}
}
}
for position in std::mem::take(&mut pending) {
let missing = io::Error::new(io::ErrorKind::UnexpectedEof, t!("The archive ends before it"));
if let Next::Stop = visit(position, Err(missing)) {
return Ok(());
}
}
pending = again;
}
Ok(())
}
fn tar_reader(archive: &Path, kind: Kind) -> io::Result<Box<dyn Read>> {
let file = open(archive)?;
Ok(match kind {
Kind::TarGz => Box::new(flate2::read::MultiGzDecoder::new(file)),
Kind::TarZst => Box::new(ZstdFrames::new(file)?),
Kind::TarXz => Box::new(lzma_rust2::XzReader::new(file, true)),
Kind::TarBz2 => Box::new(bzip2::read::MultiBzDecoder::new(file)),
_ => Box::new(file),
})
}
struct ZstdFrames<R: io::BufRead> {
frame: Option<ruzstd::decoding::StreamingDecoder<R, ruzstd::decoding::FrameDecoder>>,
}
impl<R: io::BufRead> ZstdFrames<R> {
fn new(source: R) -> io::Result<Self> {
Ok(ZstdFrames {
frame: Some(ruzstd::decoding::StreamingDecoder::new(source).map_err(io::Error::other)?),
})
}
}
impl<R: io::BufRead> Read for ZstdFrames<R> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
loop {
let Some(frame) = &mut self.frame else {
return Ok(0);
};
let read = frame.read(buf)?;
if read > 0 || buf.is_empty() {
return Ok(read);
}
let (mut source, decoder) = self.frame.take().expect("matched above").into_parts();
if source.fill_buf()?.is_empty() {
return Ok(0);
}
self.frame = Some(ruzstd::decoding::StreamingDecoder::new_with_decoder(source, decoder).map_err(io::Error::other)?);
}
}
}
pub fn scratch_dir() -> io::Result<PathBuf> {
use std::sync::atomic::{AtomicUsize, Ordering};
static NEXT: AtomicUsize = AtomicUsize::new(0);
let dir = scratch_root().join(NEXT.fetch_add(1, Ordering::Relaxed).to_string());
fs::create_dir_all(&dir)?;
Ok(dir)
}
fn scratch_root() -> PathBuf {
std::env::temp_dir().join(format!("fm84-{}", std::process::id()))
}
pub fn clean_scratch() {
let _ = fs::remove_dir_all(scratch_root());
}