use infer::get;
use sevenz_rust::{nt_time::FileTime, Password, SevenZArchiveEntry, SevenZReader, SevenZWriter};
use std::io::{self, Cursor, Read, Write};
use tar::{Archive as TarArchive, Entry as TarEntry};
use thiserror::Error;
use zip::{read::ZipFile, write::SimpleFileOptions, ZipArchive, ZipWriter};
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ArcFormat {
Zip,
Tar,
Sevenz,
}
impl TryFrom<infer::Type> for ArcFormat {
type Error = ArcError;
fn try_from(value: infer::Type) -> Result<Self, Self::Error> {
Ok(match value.extension() {
"zip" => ArcFormat::Zip,
"7z" => ArcFormat::Sevenz,
"tar" => ArcFormat::Tar,
_ => return Err(ArcError::UnrecognizedFormat),
})
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum ArcEntry {
File(String, Vec<u8>),
Directory(String),
}
impl From<ZipFile<'_>> for ArcEntry {
fn from(mut entry: ZipFile) -> Self {
if entry.is_dir() {
ArcEntry::Directory(entry.name().to_owned())
} else {
let mut data = Vec::with_capacity(entry.size() as usize);
entry.read_to_end(&mut data).unwrap();
ArcEntry::File(entry.name().to_owned(), data)
}
}
}
impl From<TarEntry<'_, &[u8]>> for ArcEntry {
fn from(mut entry: TarEntry<'_, &[u8]>) -> Self {
let name = entry.path().unwrap().to_str().unwrap().to_owned();
if entry.header().entry_type().is_dir() {
ArcEntry::Directory(name)
} else {
let mut data = Vec::with_capacity(entry.size() as usize);
entry.read_to_end(&mut data).unwrap();
ArcEntry::File(name, data)
}
}
}
#[derive(Error, Debug)]
#[error(transparent)]
pub enum ArcError {
IoError(#[from] io::Error),
ZipError(#[from] zip::result::ZipError),
SevenzError(#[from] sevenz_rust::Error),
#[error("Unrecognized archive format")]
UnrecognizedFormat,
}
pub type ArcResult<T> = Result<T, ArcError>;
pub struct ArcReader {
format: ArcFormat,
entries: Vec<ArcEntry>,
i: usize,
}
impl ArcReader {
pub fn new(buf: &[u8]) -> ArcResult<Self> {
let format = get(buf).unwrap().try_into()?;
Ok(Self {
format,
entries: match format {
ArcFormat::Zip => ArcReader::read_zip(buf),
ArcFormat::Tar => ArcReader::read_tar(buf),
ArcFormat::Sevenz => ArcReader::read_7z(buf),
}?,
i: 0,
})
}
pub fn format(&self) -> ArcFormat {
self.format
}
pub fn entries(&self) -> &Vec<ArcEntry> {
&self.entries
}
fn read_zip(buf: &[u8]) -> ArcResult<Vec<ArcEntry>> {
let mut archive = ZipArchive::new(Cursor::new(buf)).unwrap();
let len = archive.len();
let mut entries = Vec::with_capacity(len);
for i in 0..len {
entries.push(archive.by_index(i)?.into());
}
Ok(entries)
}
fn read_tar(buf: &[u8]) -> ArcResult<Vec<ArcEntry>> {
Ok(TarArchive::new(buf)
.entries()?
.map(|entry| entry.unwrap().into())
.collect())
}
fn read_7z(buf: &[u8]) -> ArcResult<Vec<ArcEntry>> {
let mut entries = Vec::new();
SevenZReader::new(Cursor::new(buf), buf.len() as u64, Password::empty())?
.for_each_entries(|entry, reader| {
if entry.is_directory {
entries.push(ArcEntry::Directory(entry.name.clone()));
} else {
let mut data = Vec::with_capacity(entry.size as usize);
reader.read_to_end(&mut data).unwrap();
entries.push(ArcEntry::File(entry.name.clone(), data));
}
Ok(true)
})
.unwrap();
Ok(entries)
}
}
impl Iterator for ArcReader {
type Item = ArcEntry;
fn next(&mut self) -> Option<Self::Item> {
if self.i == self.entries.len() {
None
} else {
self.i += 1;
Some(self.entries[self.i - 1].clone())
}
}
}
pub struct ArcWriter {
pub format: ArcFormat,
entries: Vec<ArcEntry>,
}
impl ArcWriter {
pub fn new(format: ArcFormat) -> Self {
Self {
format,
entries: Vec::new(),
}
}
pub fn push(&mut self, entry: ArcEntry) {
self.entries.push(entry)
}
pub fn extend(&mut self, entries: &[ArcEntry]) {
self.entries.extend_from_slice(entries)
}
pub fn archive(&self) -> ArcResult<Vec<u8>> {
match self.format {
ArcFormat::Zip => self.archive_zip(),
ArcFormat::Tar => self.archive_tar(),
ArcFormat::Sevenz => self.archive_7z(),
}
}
fn archive_zip(&self) -> ArcResult<Vec<u8>> {
let mut inner = Vec::new();
{
let mut writer = ZipWriter::new(Cursor::new(&mut inner));
for entry in &self.entries {
match entry {
ArcEntry::Directory(name) => {
writer.add_directory(name, SimpleFileOptions::default())?
}
ArcEntry::File(name, data) => {
writer.start_file(name.as_str(), SimpleFileOptions::default())?;
writer.write_all(data)?;
}
}
}
writer.finish()?;
}
Ok(inner)
}
#[cfg(not(target_os = "windows"))]
fn archive_tar(&self) -> ArcResult<Vec<u8>> {
use std::time::{SystemTime, UNIX_EPOCH};
use tar::{Builder as TarBuilder, Header};
use uzers::{
get_current_gid, get_current_groupname, get_current_uid, get_current_username,
};
let mut inner = Vec::new();
{
let mut builder = TarBuilder::new(&mut inner);
for entry in &self.entries {
let mut header = Header::new_gnu();
header.set_mode(0o766);
header.set_mtime(
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs(),
);
header.set_uid(get_current_uid() as u64);
header
.set_username(get_current_username().unwrap().to_str().unwrap())
.unwrap();
header.set_gid(get_current_gid() as u64);
header
.set_groupname(get_current_groupname().unwrap().to_str().unwrap())
.unwrap();
match entry {
ArcEntry::Directory(name) => {
header.set_entry_type(tar::EntryType::Directory);
builder.append_data(&mut header, name, &[][..])?;
}
ArcEntry::File(name, data) => {
header.set_entry_type(tar::EntryType::Regular);
header.set_size(data.len() as u64);
builder.append_data(&mut header, name, &data[..])?;
}
}
}
builder.finish()?;
}
Ok(inner)
}
#[cfg(target_os = "windows")]
fn archive_tar(&self) -> ArcResult<Vec<u8>> {
panic!("Cannot archive tar on Windows");
}
fn archive_7z(&self) -> ArcResult<Vec<u8>> {
let mut inner = Vec::new();
let mut archive = SevenZWriter::new(Cursor::new(&mut inner))?;
for entry in &self.entries {
let mut szentry = SevenZArchiveEntry::default();
szentry.has_last_modified_date = true;
szentry.last_modified_date = FileTime::now();
match entry {
ArcEntry::Directory(name) => {
szentry.is_directory = true;
szentry.name.clone_from(name);
archive.push_archive_entry::<&[u8]>(szentry, None)?;
}
ArcEntry::File(name, data) => {
szentry.name.clone_from(name);
archive.push_archive_entry(szentry, Some(&data[..]))?;
}
}
}
archive.finish()?;
Ok(inner)
}
}