#[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,
}
pub fn kind_of(path: &Path) -> Option<Kind> {
let name = path.file_name()?.to_string_lossy().to_lowercase();
if name.ends_with(".zip") {
Some(Kind::Zip)
} else if name.ends_with(".tar.gz") || name.ends_with(".tgz") {
Some(Kind::TarGz)
} else if name.ends_with(".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, format!("Not an archive: {}", 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)?))
}
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) => io::Error::new(io::ErrorKind::Unsupported, "Encrypted: fm84 cannot read it without the password"),
other => io::Error::new(io::ErrorKind::InvalidData, other.to_string()),
}
}
pub enum Next {
Go,
Again,
Stop,
}
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::Tar | Kind::TarGz => {
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, "The archive ends before it");
if let Next::Stop = visit(ordinal, 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)),
_ => Box::new(file),
})
}
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());
}