use std::{
fmt::{self, Display},
path::Path,
};
use tokio::io::AsyncReadExt;
use crate::error::ArchiveError;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ArchiveFormat {
Zip,
TarGz,
TarXz,
TarBz2,
TarZst,
Tar,
SevenZ,
Rar,
}
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::Rar => write!(f, "RAR"),
}
}
}
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::Rar => "rar",
}
}
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 => todo!(),
ArchiveFormat::Tar => todo!(),
ArchiveFormat::SevenZ => todo!(),
ArchiveFormat::Rar => todo!(),
}
}
}
const ZIP_MAGIC: &[u8] = &[0x50, 0x4B, 0x03, 0x04];
const GZIP_MAGIC: &[u8] = &[0x1F, 0x8B];
const XZ_MAGIC: &[u8] = &[0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00];
const BZIP2_MAGIC: &[u8] = &[0x42, 0x5A, 0x68];
const ZSTD_MAGIC: &[u8] = &[0x28, 0xB5, 0x2F, 0xFD];
const TAR_MAGIC: &[u8] = b"ustar";
const SEVENZIP_MAGIC: &[u8] = &[0x37, 0x7A, 0xBC, 0xAF, 0x27, 0x1C];
const RAR_MAGIC: &[u8] = &[0x52, 0x61, 0x72, 0x21, 0x1A, 0x07, 0x00];
fn detect_from_bytes(data: &[u8]) -> Option<ArchiveFormat> {
if data.starts_with(ZIP_MAGIC) {
Some(ArchiveFormat::Zip)
} else if data.starts_with(GZIP_MAGIC) {
Some(ArchiveFormat::TarGz)
} else if data.starts_with(XZ_MAGIC) {
Some(ArchiveFormat::TarXz)
} else if data.starts_with(BZIP2_MAGIC) {
Some(ArchiveFormat::TarBz2)
} else if data.starts_with(ZSTD_MAGIC) {
Some(ArchiveFormat::TarZst)
} else if data.starts_with(SEVENZIP_MAGIC) {
Some(ArchiveFormat::SevenZ)
} else if data.starts_with(RAR_MAGIC) {
Some(ArchiveFormat::Rar)
} else if data.len() >= 265 && &data[257..262] == TAR_MAGIC {
Some(ArchiveFormat::Tar)
} else {
None
}
}
pub(crate) 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(".rar") {
Ok(ArchiveFormat::Rar)
} else {
Err(ArchiveError::unsupported_static("format"))
}
}
pub(crate) async fn detect_from_file<P: AsRef<Path>>(
path: P,
) -> Result<ArchiveFormat, ArchiveError> {
let mut file = tokio::fs::File::open(&path).await?;
let mut buffer = [0u8; 512];
let n = file.read(&mut buffer).await?;
detect_from_bytes(&buffer[..n])
.or_else(|| detect_from_extension(path.as_ref()).ok())
.ok_or(ArchiveError::unsupported_static("format"))
}