#![allow(clippy::indexing_slicing)]
#![allow(clippy::arithmetic_side_effects)]
use std::fmt;
use std::path::Path;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize)]
pub enum ArchiveFormat {
Zip,
Tar,
TarGz,
TarBz2,
TarXz,
TarZst,
TarLz4,
SevenZip,
Gz,
Bz2,
Xz,
Lz4,
Zstd,
Lzma,
Lha,
Rar,
Arc,
Zoo,
Xar,
Dds,
StuffIt,
CompactPro,
Unknown,
}
impl fmt::Display for ArchiveFormat {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Zip => write!(f, "ZIP"),
Self::Tar => write!(f, "TAR"),
Self::TarGz => write!(f, "TAR.GZ"),
Self::TarBz2 => write!(f, "TAR.BZ2"),
Self::TarXz => write!(f, "TAR.XZ"),
Self::TarZst => write!(f, "TAR.ZSTD"),
Self::TarLz4 => write!(f, "TAR.LZ4"),
Self::SevenZip => write!(f, "7Z"),
Self::Gz => write!(f, "GZIP"),
Self::Bz2 => write!(f, "BZIP2"),
Self::Xz => write!(f, "XZ"),
Self::Lz4 => write!(f, "LZ4"),
Self::Zstd => write!(f, "ZSTD"),
Self::Lzma => write!(f, "LZMA"),
Self::Lha => write!(f, "LHA"),
Self::Rar => write!(f, "RAR"),
Self::Arc => write!(f, "ARC"),
Self::Zoo => write!(f, "ZOO"),
Self::Xar => write!(f, "XAR"),
Self::Dds => write!(f, "DDS"),
Self::StuffIt => write!(f, "StuffIt"),
Self::CompactPro => write!(f, "Compact Pro"),
Self::Unknown => write!(f, "Unknown"),
}
}
}
const ZIP_MAGIC: &[u8] = &[0x50, 0x4B, 0x03, 0x04];
const GZ_MAGIC: &[u8] = &[0x1F, 0x8B];
const BZ2_MAGIC: &[u8] = &[0x42, 0x5A, 0x68];
const XZ_MAGIC: &[u8] = &[0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00];
const SEVENZ_MAGIC: &[u8] = &[0x37, 0x7A, 0xBC, 0xAF, 0x27, 0x1C];
const ZSTD_MAGIC: &[u8] = &[0x28, 0xB5, 0x2F, 0xFD];
const LZ4_FRAME_MAGIC: &[u8] = &[0x04, 0x22, 0x4D, 0x18];
const STUFFIT_MAGIC: &[u8] = b"SIT!";
const STUFFIT5_MAGIC: &[u8] = b"StuffIt";
const COMPACT_PRO_MAGIC: u8 = 0x01;
const RAR_MAGIC: &[u8] = &[0x52, 0x61, 0x72, 0x21, 0x1A, 0x07, 0x00];
const RAR5_MAGIC: &[u8] = &[0x52, 0x61, 0x72, 0x21, 0x1A, 0x07, 0x01, 0x00];
const ZOO_MAGIC: &[u8] = &[0xDC, 0xA7, 0xC4, 0xFD];
const XAR_MAGIC: &[u8] = &[0x78, 0x61, 0x72, 0x21]; const DDS_MAGIC: &[u8] = &[0x44, 0x44, 0x53, 0x20];
const PREFIX_MAGIC_TABLE: &[(&[u8], ArchiveFormat)] = &[
(XAR_MAGIC, ArchiveFormat::Xar),
(DDS_MAGIC, ArchiveFormat::Dds),
(ZIP_MAGIC, ArchiveFormat::Zip),
(RAR5_MAGIC, ArchiveFormat::Rar),
(RAR_MAGIC, ArchiveFormat::Rar),
(SEVENZ_MAGIC, ArchiveFormat::SevenZip),
(XZ_MAGIC, ArchiveFormat::Xz),
(ZSTD_MAGIC, ArchiveFormat::Zstd),
(LZ4_FRAME_MAGIC, ArchiveFormat::Lz4),
(GZ_MAGIC, ArchiveFormat::Gz),
(BZ2_MAGIC, ArchiveFormat::Bz2),
(STUFFIT_MAGIC, ArchiveFormat::StuffIt),
(STUFFIT5_MAGIC, ArchiveFormat::StuffIt),
];
#[must_use]
pub fn detect_format(data: &[u8]) -> ArchiveFormat {
if data.len() < 4 {
return ArchiveFormat::Unknown;
}
for &(magic, format) in PREFIX_MAGIC_TABLE {
if data.starts_with(magic) {
return format;
}
}
detect_format_by_structure(data)
}
fn detect_format_by_structure(data: &[u8]) -> ArchiveFormat {
if data[0] == COMPACT_PRO_MAGIC && data[1] == 0x01 {
return ArchiveFormat::CompactPro;
}
if data.len() >= 7 && is_lha_magic(&data[2..7]) {
return ArchiveFormat::Lha;
}
if data.len() >= 24 && data[20..24] == *ZOO_MAGIC {
return ArchiveFormat::Zoo;
}
if data.len() >= 13 && is_lzma_header(data) {
return ArchiveFormat::Lzma;
}
if data[0] == 0x1A && data[1] <= 9 {
return ArchiveFormat::Arc;
}
if data.len() > 262 && &data[257..262] == b"ustar" {
return ArchiveFormat::Tar;
}
ArchiveFormat::Unknown
}
fn is_lha_magic(magic: &[u8]) -> bool {
if magic.len() < 5 || magic[0] != b'-' || magic[4] != b'-' {
return false;
}
if magic[1] == b'l' && magic[2] == b'h' {
return magic[3].is_ascii_digit()
|| magic[3] == b'd'
|| magic[3] == b's'
|| magic[3] == b'x';
}
if magic[1] == b'l' && magic[2] == b'z' {
return magic[3] == b'4' || magic[3] == b'5' || magic[3] == b's';
}
if magic[1] == b'p' && magic[2] == b'm' {
return magic[3] == b'0' || magic[3] == b'1' || magic[3] == b'2';
}
false
}
fn is_lzma_header(data: &[u8]) -> bool {
if data.len() < 13 {
return false;
}
let props = data[0];
if props > 224 {
return false;
}
let dict_size = u32::from_le_bytes([data[1], data[2], data[3], data[4]]);
if dict_size == 0 {
return false;
}
let max_dict = 1_610_612_736; if dict_size > max_dict {
return false;
}
props == 0x5D || (dict_size.is_power_of_two() && dict_size >= 4096)
}
const EXTENSION_TABLE: &[(&[&str], ArchiveFormat)] = &[
(&[".tar.gz", ".tgz"], ArchiveFormat::TarGz),
(&[".tar.bz2", ".tbz2"], ArchiveFormat::TarBz2),
(&[".tar.xz", ".txz"], ArchiveFormat::TarXz),
(&[".tar.zst", ".tzst"], ArchiveFormat::TarZst),
(&[".tar.lz4"], ArchiveFormat::TarLz4),
(&[".tar"], ArchiveFormat::Tar),
(&[".sit", ".sitx"], ArchiveFormat::StuffIt),
(&[".cpt"], ArchiveFormat::CompactPro),
(&[".7z"], ArchiveFormat::SevenZip),
(&[".lzma"], ArchiveFormat::Lzma),
(&[".lzh", ".lha"], ArchiveFormat::Lha),
(&[".rar"], ArchiveFormat::Rar),
(&[".arc"], ArchiveFormat::Arc),
(&[".zoo"], ArchiveFormat::Zoo),
(&[".xar", ".pkg"], ArchiveFormat::Xar),
(&[".dds"], ArchiveFormat::Dds),
];
#[must_use]
#[allow(clippy::case_sensitive_file_extension_comparisons)]
pub fn detect_format_from_path(path: &Path, data: &[u8]) -> ArchiveFormat {
let magic = detect_format(data);
if !matches!(
magic,
ArchiveFormat::Gz
| ArchiveFormat::Bz2
| ArchiveFormat::Xz
| ArchiveFormat::Zstd
| ArchiveFormat::Lz4
| ArchiveFormat::Unknown
) {
return magic;
}
let name = path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("")
.to_ascii_lowercase();
for &(suffixes, format) in EXTENSION_TABLE {
if suffixes.iter().any(|suffix| name.ends_with(suffix)) {
return format;
}
}
magic
}
#[derive(Debug, serde::Serialize)]
pub struct FormatInfo {
pub format: ArchiveFormat,
pub description: String,
pub mime_type: String,
pub is_archive: bool,
pub is_compressed: bool,
}
#[must_use]
pub fn format_info(format: ArchiveFormat) -> FormatInfo {
match format {
ArchiveFormat::Zip => FormatInfo {
format,
description: "ZIP archive (PKZIP compatible)".into(),
mime_type: "application/zip".into(),
is_archive: true,
is_compressed: true,
},
ArchiveFormat::Tar => FormatInfo {
format,
description: "POSIX TAR archive (uncompressed)".into(),
mime_type: "application/x-tar".into(),
is_archive: true,
is_compressed: false,
},
ArchiveFormat::TarGz => FormatInfo {
format,
description: "TAR archive compressed with gzip".into(),
mime_type: "application/gzip".into(),
is_archive: true,
is_compressed: true,
},
ArchiveFormat::TarBz2 => FormatInfo {
format,
description: "TAR archive compressed with bzip2".into(),
mime_type: "application/x-bzip2".into(),
is_archive: true,
is_compressed: true,
},
ArchiveFormat::TarXz => FormatInfo {
format,
description: "TAR archive compressed with XZ/LZMA2".into(),
mime_type: "application/x-xz".into(),
is_archive: true,
is_compressed: true,
},
ArchiveFormat::TarZst => FormatInfo {
format,
description: "TAR archive compressed with Zstandard".into(),
mime_type: "application/zstd".into(),
is_archive: true,
is_compressed: true,
},
ArchiveFormat::TarLz4 => FormatInfo {
format,
description: "TAR archive compressed with LZ4".into(),
mime_type: "application/x-lz4".into(),
is_archive: true,
is_compressed: true,
},
ArchiveFormat::SevenZip => FormatInfo {
format,
description: "7-Zip archive (LZMA/LZMA2)".into(),
mime_type: "application/x-7z-compressed".into(),
is_archive: true,
is_compressed: true,
},
ArchiveFormat::Gz => FormatInfo {
format,
description: "Gzip compressed file (RFC 1952)".into(),
mime_type: "application/gzip".into(),
is_archive: false,
is_compressed: true,
},
ArchiveFormat::Bz2 => FormatInfo {
format,
description: "Bzip2 compressed file".into(),
mime_type: "application/x-bzip2".into(),
is_archive: false,
is_compressed: true,
},
ArchiveFormat::Xz => FormatInfo {
format,
description: "XZ compressed file (LZMA2)".into(),
mime_type: "application/x-xz".into(),
is_archive: false,
is_compressed: true,
},
ArchiveFormat::Lz4 => FormatInfo {
format,
description: "LZ4 compressed file (frame format)".into(),
mime_type: "application/x-lz4".into(),
is_archive: false,
is_compressed: true,
},
ArchiveFormat::Zstd => FormatInfo {
format,
description: "Zstandard compressed file".into(),
mime_type: "application/zstd".into(),
is_archive: false,
is_compressed: true,
},
ArchiveFormat::Lzma => FormatInfo {
format,
description: "LZMA compressed file (standalone)".into(),
mime_type: "application/x-lzma".into(),
is_archive: false,
is_compressed: true,
},
ArchiveFormat::Lha => FormatInfo {
format,
description: "LHA/LZH archive".into(),
mime_type: "application/x-lzh-compressed".into(),
is_archive: true,
is_compressed: true,
},
ArchiveFormat::Rar => FormatInfo {
format,
description: "RAR archive".into(),
mime_type: "application/vnd.rar".into(),
is_archive: true,
is_compressed: true,
},
ArchiveFormat::Arc => FormatInfo {
format,
description: "ARC archive".into(),
mime_type: "application/x-arc".into(),
is_archive: true,
is_compressed: true,
},
ArchiveFormat::Zoo => FormatInfo {
format,
description: "ZOO archive".into(),
mime_type: "application/x-zoo".into(),
is_archive: true,
is_compressed: true,
},
ArchiveFormat::Xar => FormatInfo {
format,
description: "XAR archive (eXtensible ARchive)".into(),
mime_type: "application/x-xar".into(),
is_archive: true,
is_compressed: true,
},
ArchiveFormat::Dds => FormatInfo {
format,
description: "DDS texture (DirectDraw Surface)".into(),
mime_type: "image/vnd-ms.dds".into(),
is_archive: false,
is_compressed: true,
},
ArchiveFormat::StuffIt => FormatInfo {
format,
description: "StuffIt archive (classic Mac OS)".into(),
mime_type: "application/x-stuffit".into(),
is_archive: true,
is_compressed: true,
},
ArchiveFormat::CompactPro => FormatInfo {
format,
description: "Compact Pro archive (classic Mac OS)".into(),
mime_type: "application/x-compact-pro".into(),
is_archive: true,
is_compressed: true,
},
ArchiveFormat::Unknown => FormatInfo {
format,
description: "Unknown format".into(),
mime_type: "application/octet-stream".into(),
is_archive: false,
is_compressed: false,
},
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_detect_zip() {
let data = [0x50, 0x4B, 0x03, 0x04, 0x00, 0x00];
assert_eq!(detect_format(&data), ArchiveFormat::Zip);
}
#[test]
fn test_detect_gzip() {
let data = [0x1F, 0x8B, 0x08, 0x00];
assert_eq!(detect_format(&data), ArchiveFormat::Gz);
}
#[test]
fn test_detect_xz() {
let data = [0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00, 0x00];
assert_eq!(detect_format(&data), ArchiveFormat::Xz);
}
#[test]
fn test_detect_lz4_frame() {
let data = [0x04, 0x22, 0x4D, 0x18, 0x00];
assert_eq!(detect_format(&data), ArchiveFormat::Lz4);
}
#[test]
fn test_detect_zstd() {
let data = [0x28, 0xB5, 0x2F, 0xFD, 0x00];
assert_eq!(detect_format(&data), ArchiveFormat::Zstd);
}
#[test]
fn test_detect_bz2() {
let data = [0x42, 0x5A, 0x68, 0x39, 0x00];
assert_eq!(detect_format(&data), ArchiveFormat::Bz2);
}
#[test]
fn test_detect_7z() {
let data = [0x37, 0x7A, 0xBC, 0xAF, 0x27, 0x1C, 0x00];
assert_eq!(detect_format(&data), ArchiveFormat::SevenZip);
}
#[test]
fn test_detect_stuffit() {
let data = b"SIT!\x00\x00\x00\x00";
assert_eq!(detect_format(data), ArchiveFormat::StuffIt);
}
#[test]
fn test_detect_compact_pro() {
let data = [0x01, 0x01, 0x00, 0x00, 0x00, 0x00];
assert_eq!(detect_format(&data), ArchiveFormat::CompactPro);
}
#[test]
fn test_detect_rar3() {
let data = [0x52, 0x61, 0x72, 0x21, 0x1A, 0x07, 0x00, 0x00];
assert_eq!(detect_format(&data), ArchiveFormat::Rar);
}
#[test]
fn test_detect_rar5() {
let data = [0x52, 0x61, 0x72, 0x21, 0x1A, 0x07, 0x01, 0x00, 0x00];
assert_eq!(detect_format(&data), ArchiveFormat::Rar);
}
#[test]
fn test_detect_lha() {
let data = [0x20, 0x00, b'-', b'l', b'h', b'5', b'-'];
assert_eq!(detect_format(&data), ArchiveFormat::Lha);
}
#[test]
fn test_detect_lha_lz() {
let data = [0x20, 0x00, b'-', b'l', b'z', b'5', b'-'];
assert_eq!(detect_format(&data), ArchiveFormat::Lha);
}
#[test]
fn test_detect_lzma() {
let data = [
0x5D, 0x00, 0x00, 0x80, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
];
assert_eq!(detect_format(&data), ArchiveFormat::Lzma);
}
#[test]
fn test_detect_arc() {
let data = [0x1A, 0x02, 0x00, 0x00];
assert_eq!(detect_format(&data), ArchiveFormat::Arc);
}
#[test]
fn test_detect_zoo() {
let mut data = [0u8; 25];
data[20] = 0xDC;
data[21] = 0xA7;
data[22] = 0xC4;
data[23] = 0xFD;
assert_eq!(detect_format(&data), ArchiveFormat::Zoo);
}
#[test]
fn test_detect_xar() {
let data = [0x78, 0x61, 0x72, 0x21, 0x00, 0x00];
assert_eq!(detect_format(&data), ArchiveFormat::Xar);
}
#[test]
fn test_detect_dds() {
let data = [0x44, 0x44, 0x53, 0x20, 0x00, 0x00];
assert_eq!(detect_format(&data), ArchiveFormat::Dds);
}
#[test]
fn test_extension_xar() {
let data = [0x78, 0x61, 0x72, 0x21, 0x00, 0x00];
let path = Path::new("test.xar");
assert_eq!(detect_format_from_path(path, &data), ArchiveFormat::Xar);
}
#[test]
fn test_extension_pkg() {
let data = [0x78, 0x61, 0x72, 0x21, 0x00, 0x00];
let path = Path::new("installer.pkg");
assert_eq!(detect_format_from_path(path, &data), ArchiveFormat::Xar);
}
#[test]
fn test_extension_dds() {
let data = [0x44, 0x44, 0x53, 0x20, 0x00, 0x00];
let path = Path::new("texture.dds");
assert_eq!(detect_format_from_path(path, &data), ArchiveFormat::Dds);
}
#[test]
fn test_detect_unknown() {
let data = [0xFF, 0xFE, 0x00, 0x00];
assert_eq!(detect_format(&data), ArchiveFormat::Unknown);
}
#[test]
fn test_detect_too_short() {
let data = [0x50, 0x4B];
assert_eq!(detect_format(&data), ArchiveFormat::Unknown);
}
#[test]
fn test_extension_lzma() {
let data = [0x00; 16];
let path = Path::new("test.lzma");
assert_eq!(detect_format_from_path(path, &data), ArchiveFormat::Lzma);
}
#[test]
fn test_extension_lzh() {
let data = [0x00; 16];
let path = Path::new("test.lzh");
assert_eq!(detect_format_from_path(path, &data), ArchiveFormat::Lha);
}
#[test]
fn test_extension_rar() {
let data = [0x00; 16];
let path = Path::new("test.rar");
assert_eq!(detect_format_from_path(path, &data), ArchiveFormat::Rar);
}
#[test]
fn test_extension_tar_gz() {
let data = [0x1F, 0x8B, 0x08, 0x00, 0x00];
let path = Path::new("test.tar.gz");
assert_eq!(detect_format_from_path(path, &data), ArchiveFormat::TarGz);
}
#[test]
fn test_extension_tgz() {
let data = [0x1F, 0x8B, 0x08, 0x00, 0x00];
let path = Path::new("test.tgz");
assert_eq!(detect_format_from_path(path, &data), ArchiveFormat::TarGz);
}
}