use super::sevenzip::{ArchiveCompression, ArchiveType, tool};
use anyhow::{Context, Result};
use sevenz_rust2::encoder_options::{Lzma2Options, ZstandardOptions};
use sevenz_rust2::{ArchiveEntry, ArchiveReader, ArchiveWriter, EncoderMethod, Password};
use std::fs::File;
use std::io;
use std::path::Path;
use std::str::FromStr;
use tokio::task::spawn_blocking;
use zip::write::SimpleFileOptions;
use zip::{CompressionMethod, ZipArchive, ZipWriter};
fn archive_type(path: &Path) -> Result<ArchiveType> {
let extension = path
.extension()
.and_then(|extension| extension.to_str())
.unwrap_or_default()
.to_lowercase();
ArchiveType::from_str(&extension).context("Not a valid archive")
}
pub async fn parse<P: AsRef<Path>>(
path: P,
name: Option<&str>,
) -> Result<Vec<(String, u64, String)>> {
let path = path.as_ref().to_path_buf();
let name = name.map(|name| name.to_string());
spawn_blocking(move || {
let wanted = |entry: &str| name.as_ref().is_none_or(|name| name == entry);
match archive_type(&path)? {
ArchiveType::Sevenzip => {
let archive = sevenz_rust2::Archive::open(&path)
.with_context(|| format!("Failed to open \"{}\"", path.display()))?;
Ok(archive
.files
.iter()
.filter(|entry| !entry.is_directory && wanted(&entry.name))
.map(|entry| {
(
entry.name.clone(),
entry.size,
format!("{:08x}", entry.crc as u32),
)
})
.collect())
}
ArchiveType::Zip => {
let file = File::open(&path)
.with_context(|| format!("Failed to open \"{}\"", path.display()))?;
let mut archive = ZipArchive::new(file)
.with_context(|| format!("Failed to read \"{}\"", path.display()))?;
let mut entries = Vec::with_capacity(archive.len());
for index in 0..archive.len() {
let entry = archive.by_index(index)?;
if entry.is_dir() || !wanted(entry.name()) {
continue;
}
entries.push((
entry.name().to_string(),
entry.size(),
format!("{:08x}", entry.crc32()),
));
}
Ok(entries)
}
}
})
.await
.context("Archive task failed")?
}
pub async fn extract<P: AsRef<Path>, Q: AsRef<Path>>(
path: P,
name: &str,
directory: Q,
) -> Result<()> {
let path = path.as_ref().to_path_buf();
let name = name.to_string();
let directory = directory.as_ref().to_path_buf();
spawn_blocking(move || {
let destination = directory.join(&name);
if let Some(parent) = destination.parent() {
std::fs::create_dir_all(parent)?;
}
match archive_type(&path)? {
ArchiveType::Sevenzip => {
let mut reader = ArchiveReader::open(&path, Password::empty())
.with_context(|| format!("Failed to open \"{}\"", path.display()))?;
reader.set_thread_count(threads());
let data = reader
.read_file(&name)
.with_context(|| format!("Failed to extract \"{}\"", name))?;
std::fs::write(&destination, data)?;
}
ArchiveType::Zip => {
let mut archive = ZipArchive::new(File::open(&path)?)
.with_context(|| format!("Failed to read \"{}\"", path.display()))?;
let mut entry = archive
.by_name(&name)
.with_context(|| format!("Failed to extract \"{}\"", name))?;
io::copy(&mut entry, &mut File::create(&destination)?)?;
}
}
Ok(())
})
.await
.context("Archive task failed")?
}
pub async fn create<P: AsRef<Path>, Q: AsRef<Path>>(
archive_path: P,
working_directory: Q,
entry_path: &Path,
archive_type: &ArchiveType,
compression: &ArchiveCompression,
solid: bool,
) -> Result<()> {
if archive_type == &ArchiveType::Sevenzip
&& archive_path.as_ref().is_file()
&& !compression.is_zstd()
&& !is_zstd(archive_path.as_ref())
{
return tool::create(
archive_path,
working_directory,
entry_path,
archive_type,
compression,
solid,
)
.await;
}
let archive_path = archive_path.as_ref().to_path_buf();
let working_directory = working_directory.as_ref().to_path_buf();
let entry_path = entry_path.to_path_buf();
let archive_type = *archive_type;
let compression = *compression;
spawn_blocking(move || {
let name = entry_path.to_string_lossy().replace('\\', "/");
let source = working_directory.join(&entry_path);
match archive_type {
ArchiveType::Zip => {
let (method, level) = match compression {
ArchiveCompression::Zstd(level) => {
(CompressionMethod::Zstd, Some(level as i64))
}
ArchiveCompression::Deflate(level) => {
(CompressionMethod::Deflated, Some(level as i64))
}
_ => (CompressionMethod::Deflated, None),
};
let options = SimpleFileOptions::default()
.compression_method(method)
.compression_level(level)
.large_file(true);
let mut writer = if archive_path.is_file() {
ZipWriter::new_append(
File::options().read(true).write(true).open(&archive_path)?,
)?
} else {
ZipWriter::new(File::create(&archive_path)?)
};
writer.start_file(&name, options)?;
io::copy(&mut File::open(&source)?, &mut writer)?;
writer.finish()?;
}
ArchiveType::Sevenzip => {
write_sevenzip(
&archive_path,
vec![(name, std::fs::read(&source)?)],
compression,
solid,
)?;
}
}
Ok(())
})
.await
.context("Archive task failed")?
}
pub async fn rename<P: AsRef<Path>>(path: P, from: &str, to: &str) -> Result<()> {
if archive_type(path.as_ref())? == ArchiveType::Sevenzip && !is_zstd(path.as_ref()) {
return tool::rename(path, from, to).await;
}
let (from, to) = (from.to_string(), to.to_string());
rebuild(path, move |name| {
if name == from {
Some(to.clone())
} else {
Some(name.to_string())
}
})
.await
}
pub async fn delete<P: AsRef<Path>>(path: P, name: &str) -> Result<()> {
if archive_type(path.as_ref())? == ArchiveType::Sevenzip && !is_zstd(path.as_ref()) {
return tool::delete(path, name).await;
}
let name = name.to_string();
rebuild(path, move |entry| {
if entry == name {
None
} else {
Some(entry.to_string())
}
})
.await
}
async fn rebuild<P, F>(path: P, map: F) -> Result<()>
where
P: AsRef<Path>,
F: Fn(&str) -> Option<String> + Send + 'static,
{
let path = path.as_ref().to_path_buf();
spawn_blocking(move || {
let temporary = path.with_extension("oxyromon-tmp");
match archive_type(&path)? {
ArchiveType::Zip => {
let mut archive = ZipArchive::new(File::open(&path)?)
.with_context(|| format!("Failed to read \"{}\"", path.display()))?;
let mut writer = ZipWriter::new(File::create(&temporary)?);
for index in 0..archive.len() {
let entry = archive.by_index_raw(index)?;
match map(entry.name()) {
Some(name) if name == entry.name() => writer.raw_copy_file(entry)?,
Some(name) => writer.raw_copy_file_rename(entry, name)?,
None => {}
}
}
writer.finish()?;
}
ArchiveType::Sevenzip => {
let solid = sevenz_rust2::Archive::open(&path)?.is_solid;
let compression = if is_zstd(&path) {
ArchiveCompression::Zstd(DEFAULT_ZSTD_LEVEL)
} else {
ArchiveCompression::Default
};
let entries: Vec<(String, Vec<u8>)> = read_sevenzip(&path)?
.into_iter()
.filter_map(|(name, data)| map(&name).map(|name| (name, data)))
.collect();
if entries.is_empty() {
std::fs::remove_file(&path)?;
return Ok(());
}
write_sevenzip(&temporary, entries, compression, solid)?;
}
}
std::fs::rename(&temporary, &path)?;
Ok(())
})
.await
.context("Archive task failed")?
}
const DEFAULT_COMPRESSION_LEVEL: usize = 9;
const DEFAULT_ZSTD_LEVEL: usize = 19;
fn threads() -> u32 {
std::thread::available_parallelism()
.map(|available| available.get() as u32)
.unwrap_or(1)
}
fn is_zstd(path: &Path) -> bool {
sevenz_rust2::Archive::open(path)
.map(|archive| {
archive.blocks.iter().any(|block| {
block
.coders
.iter()
.any(|coder| coder.encoder_method_id() == EncoderMethod::ID_ZSTD)
})
})
.unwrap_or(false)
}
fn read_sevenzip(path: &Path) -> Result<Vec<(String, Vec<u8>)>> {
let mut reader = ArchiveReader::open(path, Password::empty())
.with_context(|| format!("Failed to open \"{}\"", path.display()))?;
reader.set_thread_count(threads());
let mut entries = Vec::new();
reader.for_each_entries(|entry, reader| {
if entry.is_directory {
return Ok(true);
}
let mut data = Vec::with_capacity(entry.size as usize);
io::copy(reader, &mut data)?;
entries.push((entry.name.clone(), data));
Ok(true)
})?;
Ok(entries)
}
fn write_sevenzip(
path: &Path,
entries: Vec<(String, Vec<u8>)>,
compression: ArchiveCompression,
solid: bool,
) -> Result<()> {
let mut writer = ArchiveWriter::create(path)
.with_context(|| format!("Failed to create \"{}\"", path.display()))?;
writer.set_content_methods(vec![match compression {
ArchiveCompression::Zstd(level) => ZstandardOptions::from_level(level as u32).into(),
ArchiveCompression::Lzma2(level) => {
Lzma2Options::from_level_mt(level as u32, threads(), 1).into()
}
_ => Lzma2Options::from_level_mt(DEFAULT_COMPRESSION_LEVEL as u32, threads(), 1).into(),
}]);
if solid {
writer.push_archive_entries(
entries
.iter()
.map(|(name, _)| ArchiveEntry::new_file(name))
.collect(),
entries
.iter()
.map(|(_, data)| io::Cursor::new(data.as_slice()).into())
.collect(),
)?;
} else {
for (name, data) in &entries {
writer.push_archive_entry(
ArchiveEntry::new_file(name),
Some(io::Cursor::new(data.as_slice())),
)?;
}
}
writer.finish()?;
Ok(())
}