use std::{
fmt::{self, Display},
fs::File,
io::Read,
path::Path,
};
use crate::error::ArchiveError;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ArchiveFormat {
Zip,
TarGz,
TarXz,
TarBz2,
TarZst,
Tar,
SevenZ,
Gz,
Xz,
Bz2,
Zst,
}
impl Display for ArchiveFormat {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ArchiveFormat::Zip => write!(f, "ZIP"),
ArchiveFormat::Tar => write!(f, "TAR"),
ArchiveFormat::TarGz => write!(f, "TAR.GZ"),
ArchiveFormat::TarBz2 => write!(f, "TAR.BZ2"),
ArchiveFormat::TarXz => write!(f, "TAR.XZ"),
ArchiveFormat::TarZst => write!(f, "TAR.ZST"),
ArchiveFormat::SevenZ => write!(f, "7Z"),
ArchiveFormat::Gz => write!(f, "GZ"),
ArchiveFormat::Xz => write!(f, "XZ"),
ArchiveFormat::Bz2 => write!(f, "BZ2"),
ArchiveFormat::Zst => write!(f, "ZST"),
}
}
}
impl ArchiveFormat {
pub fn extension(&self) -> &'static str {
match self {
ArchiveFormat::Zip => "zip",
ArchiveFormat::Tar => "tar",
ArchiveFormat::TarGz => "tar.gz",
ArchiveFormat::TarBz2 => "tar.bz2",
ArchiveFormat::TarXz => "tar.xz",
ArchiveFormat::TarZst => "tar.zst",
ArchiveFormat::SevenZ => "7z",
ArchiveFormat::Gz => "gz",
ArchiveFormat::Xz => "xz",
ArchiveFormat::Bz2 => "bz2",
ArchiveFormat::Zst => "zst",
}
}
pub fn mime_type(&self) -> &'static str {
match self {
ArchiveFormat::Zip => "application/zip",
ArchiveFormat::TarGz => "application/gzip",
ArchiveFormat::TarXz => "application/x-xz",
ArchiveFormat::TarBz2 => "application/x-bzip2",
ArchiveFormat::TarZst => "application/zstd",
ArchiveFormat::Tar => "application/x-tar",
ArchiveFormat::SevenZ => "application/x-7z-compressed",
ArchiveFormat::Gz => "application/gzip",
ArchiveFormat::Xz => "application/x-xz",
ArchiveFormat::Bz2 => "application/x-bzip2",
ArchiveFormat::Zst => "application/zstd",
}
}
}
const ZIP_SIGNATURE: &[u8] = &[0x50, 0x4B, 0x03, 0x04];
const GZIP_SIGNATURE: &[u8] = &[0x1F, 0x8B];
const XZ_SIGNATURE: &[u8] = &[0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00];
const BZIP2_SIGNATURE: &[u8] = &[0x42, 0x5A, 0x68];
const ZSTD_SIGNATURE: &[u8] = &[0x28, 0xB5, 0x2F, 0xFD];
const TAR_SIGNATURE: &[u8] = &[0x75, 0x73, 0x74, 0x61, 0x72];
const SEVENZIP_SIGNATURE: &[u8] = &[0x37, 0x7A, 0xBC, 0xAF, 0x27, 0x1C];
pub fn detect_from_bytes(data: &[u8]) -> Option<ArchiveFormat> {
if data.starts_with(ZIP_SIGNATURE) {
Some(ArchiveFormat::Zip)
} else if data.starts_with(GZIP_SIGNATURE) {
Some(ArchiveFormat::TarGz)
} else if data.starts_with(XZ_SIGNATURE) {
Some(ArchiveFormat::TarXz)
} else if data.starts_with(BZIP2_SIGNATURE) {
Some(ArchiveFormat::TarBz2)
} else if data.starts_with(ZSTD_SIGNATURE) {
Some(ArchiveFormat::TarZst)
} else if data.starts_with(SEVENZIP_SIGNATURE) {
Some(ArchiveFormat::SevenZ)
} else if data.len() >= 265 && &data[257..262] == TAR_SIGNATURE {
Some(ArchiveFormat::Tar)
} else {
None
}
}
pub fn detect_from_extension<P: AsRef<Path>>(path: P) -> Result<ArchiveFormat, ArchiveError> {
let path_str = path.as_ref().to_string_lossy().to_lowercase();
if path_str.ends_with(".tar.gz") || path_str.ends_with(".tgz") {
Ok(ArchiveFormat::TarGz)
} else if path_str.ends_with(".tar.xz") || path_str.ends_with(".txz") {
Ok(ArchiveFormat::TarXz)
} else if path_str.ends_with(".tar.bz2") || path_str.ends_with(".tbz2") {
Ok(ArchiveFormat::TarBz2)
} else if path_str.ends_with(".tar.zst") {
Ok(ArchiveFormat::TarZst)
} else if path_str.ends_with(".tar") {
Ok(ArchiveFormat::Tar)
} else if path_str.ends_with(".zip") {
Ok(ArchiveFormat::Zip)
} else if path_str.ends_with(".7z") {
Ok(ArchiveFormat::SevenZ)
} else if path_str.ends_with(".gz") {
Ok(ArchiveFormat::Gz)
} else if path_str.ends_with(".xz") {
Ok(ArchiveFormat::Xz)
} else if path_str.ends_with(".bz2") {
Ok(ArchiveFormat::Bz2)
} else if path_str.ends_with(".zst") {
Ok(ArchiveFormat::Zst)
} else {
Err(ArchiveError::unsupported_static("format"))
}
}
pub fn detect_from_file<P: AsRef<Path>>(path: P) -> Result<ArchiveFormat, ArchiveError> {
let mut file = File::open(&path)?;
let mut buffer = [0u8; 512];
let n = file.read(&mut buffer)?;
let format = detect_from_bytes(&buffer[..n])
.or_else(|| detect_from_extension(path.as_ref()).ok())
.ok_or(ArchiveError::unsupported_static("format"))?;
Ok(refine_compressed(path.as_ref(), format))
}
fn refine_compressed(path: &Path, format: ArchiveFormat) -> ArchiveFormat {
let bare = match format {
ArchiveFormat::TarGz => ArchiveFormat::Gz,
ArchiveFormat::TarXz => ArchiveFormat::Xz,
ArchiveFormat::TarBz2 => ArchiveFormat::Bz2,
ArchiveFormat::TarZst => ArchiveFormat::Zst,
other => return other,
};
match reads_as_tar(path, format) {
Some(true) | None => format,
Some(false) => bare,
}
}
fn reads_as_tar(path: &Path, format: ArchiveFormat) -> Option<bool> {
let file = File::open(path).ok()?;
let mut reader: Box<dyn Read> = match format {
ArchiveFormat::TarGz => Box::new(flate2::read::GzDecoder::new(file)),
ArchiveFormat::TarXz => Box::new(liblzma::read::XzDecoder::new(file)),
ArchiveFormat::TarBz2 => Box::new(bzip2::read::BzDecoder::new(file)),
ArchiveFormat::TarZst => Box::new(zstd::stream::read::Decoder::new(file).ok()?),
_ => return None,
};
let mut head = [0u8; 512];
let mut filled = 0;
while filled < head.len() {
match reader.read(&mut head[filled..]) {
Ok(0) => break,
Ok(n) => filled += n,
Err(_) => return None,
}
}
if filled < 262 {
return Some(false);
}
Some(&head[257..262] == TAR_SIGNATURE)
}