mirror of
https://github.com/allaunthefox/Research-Stack.git
synced 2026-07-31 03:05:21 +00:00
483 lines
16 KiB
Python
483 lines
16 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
# Copyright (c) 2025 Allaun
|
|
|
|
"""Compression Engine Module.
|
|
|
|
Compression and archive utilities using standard library only.
|
|
|
|
Key Features:
|
|
- Data compression (gzip, bz2, lzma)
|
|
- File compression and decompression
|
|
- Archive creation and extraction (zip, tar, tar.gz, tar.bz2, tar.xz)
|
|
- Path validation for security
|
|
- Compression ratio estimation
|
|
- Standard library only (no external dependencies)
|
|
|
|
Dependencies:
|
|
- gzip (standard library)
|
|
- bz2 (standard library)
|
|
- lzma (standard library)
|
|
- tarfile (standard library)
|
|
- zipfile (standard library)
|
|
- pathlib (standard library)
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
import gzip
|
|
import bz2
|
|
import lzma
|
|
import tarfile
|
|
import zipfile
|
|
from pathlib import Path
|
|
from typing import Optional, List, Union
|
|
|
|
|
|
PathLike = Union[str, Path]
|
|
|
|
|
|
class CompressionError(Exception):
|
|
"""Exception raised for compression and archive operations.
|
|
|
|
Attributes:
|
|
message: Explanation of the error
|
|
"""
|
|
pass
|
|
|
|
|
|
class Compression:
|
|
"""Compression and archive operations handler.
|
|
|
|
Provides static methods for compressing/decompressing data and files,
|
|
as well as creating and extracting archives in various formats.
|
|
|
|
Attributes:
|
|
DATA_FORMATS: List of supported compression formats
|
|
EXTENSION_MAP: Mapping of file extensions to format names
|
|
FORMAT_EXTENSION: Mapping of format names to file extensions
|
|
TAR_MODE_MAP: Mapping of tar format names to tarfile modes
|
|
COMPRESSION_RATIOS: Estimated compression ratios by format and data type
|
|
"""
|
|
|
|
DATA_FORMATS = ['gzip', 'bz2', 'lzma']
|
|
EXTENSION_MAP = {
|
|
'.gz': 'gzip',
|
|
'.bz2': 'bz2',
|
|
'.xz': 'lzma',
|
|
'.zip': 'zip'
|
|
}
|
|
|
|
FORMAT_EXTENSION = {
|
|
'gzip': '.gz',
|
|
'bz2': '.bz2',
|
|
'lzma': '.xz',
|
|
'zip': '.zip'
|
|
}
|
|
|
|
TAR_MODE_MAP = {
|
|
'tar': 'w',
|
|
'tar.gz': 'w:gz',
|
|
'tar.bz2': 'w:bz2',
|
|
'tar.xz': 'w:xz'
|
|
}
|
|
|
|
COMPRESSION_RATIOS = {
|
|
'gzip': {'text': 0.3, 'binary': 0.6, 'image': 0.9, 'video': 0.95},
|
|
'gz': {'text': 0.3, 'binary': 0.6, 'image': 0.9, 'video': 0.95},
|
|
'bz2': {'text': 0.25, 'binary': 0.55, 'image': 0.9, 'video': 0.95},
|
|
'lzma': {'text': 0.2, 'binary': 0.5, 'image': 0.9, 'video': 0.95},
|
|
'xz': {'text': 0.2, 'binary': 0.5, 'image': 0.9, 'video': 0.95},
|
|
}
|
|
|
|
@staticmethod
|
|
def _ensure_path(path: PathLike) -> Path:
|
|
"""Ensure path is a Path object.
|
|
|
|
Args:
|
|
path: Path as string or Path object
|
|
|
|
Returns:
|
|
Path object
|
|
"""
|
|
return Path(path)
|
|
|
|
@staticmethod
|
|
def _validate_extraction_path(output_dir: Path, member_name: str) -> None:
|
|
"""Validate extraction path to prevent directory traversal attacks.
|
|
|
|
Args:
|
|
output_dir: Target extraction directory
|
|
member_name: Name of archive member to extract
|
|
|
|
Raises:
|
|
CompressionError: If the extraction path is unsafe
|
|
"""
|
|
member_path = (output_dir / member_name).resolve()
|
|
output_dir_resolved = output_dir.resolve()
|
|
try:
|
|
member_path.relative_to(output_dir_resolved)
|
|
except ValueError:
|
|
raise CompressionError(
|
|
f"Unsafe extraction path: {member_name}"
|
|
)
|
|
|
|
@staticmethod
|
|
def _validate_format(format: str) -> None:
|
|
"""Validate that the format is supported.
|
|
|
|
Args:
|
|
format: Compression or archive format name
|
|
|
|
Raises:
|
|
CompressionError: If format is not supported
|
|
"""
|
|
if format not in Compression.DATA_FORMATS and \
|
|
format not in Compression.FORMAT_EXTENSION and \
|
|
format not in Compression.TAR_MODE_MAP:
|
|
raise CompressionError(f"Unsupported format: {format}")
|
|
|
|
@staticmethod
|
|
def compress_data(data: bytes, format: str = 'gzip', level: int = 6) -> bytes:
|
|
"""Compress raw bytes data.
|
|
|
|
Args:
|
|
data: Bytes to compress
|
|
format: Compression format ('gzip', 'bz2', or 'lzma')
|
|
level: Compression level (1-9 for gzip/bz2, 0-9 for lzma)
|
|
|
|
Returns:
|
|
Compressed bytes
|
|
|
|
Raises:
|
|
CompressionError: If compression fails or format is unsupported
|
|
"""
|
|
Compression._validate_format(format)
|
|
|
|
try:
|
|
if format == 'gzip':
|
|
return gzip.compress(data, compresslevel=level)
|
|
if format == 'bz2':
|
|
return bz2.compress(data, compresslevel=level)
|
|
if format == 'lzma':
|
|
return lzma.compress(data, preset=level)
|
|
except Exception as e:
|
|
raise CompressionError(f"Compression failed: {e}") from e
|
|
|
|
raise CompressionError(f"Unsupported format: {format}")
|
|
|
|
@staticmethod
|
|
def decompress_data(data: bytes, format: str = 'gzip') -> bytes:
|
|
"""Decompress compressed bytes data.
|
|
|
|
Args:
|
|
data: Compressed bytes to decompress
|
|
format: Compression format ('gzip', 'bz2', or 'lzma')
|
|
|
|
Returns:
|
|
Decompressed bytes
|
|
|
|
Raises:
|
|
CompressionError: If decompression fails or format is unsupported
|
|
"""
|
|
Compression._validate_format(format)
|
|
|
|
try:
|
|
if format == 'gzip':
|
|
return gzip.decompress(data)
|
|
if format == 'bz2':
|
|
return bz2.decompress(data)
|
|
if format == 'lzma':
|
|
return lzma.decompress(data)
|
|
except Exception as e:
|
|
raise CompressionError(f"Decompression failed: {e}") from e
|
|
|
|
raise CompressionError(f"Unsupported format: {format}")
|
|
|
|
@staticmethod
|
|
def compress_file(
|
|
input_path: PathLike,
|
|
output_path: Optional[PathLike] = None,
|
|
format: str = 'gzip',
|
|
remove_original: bool = False
|
|
) -> Path:
|
|
"""Compress a file.
|
|
|
|
Args:
|
|
input_path: Path to the input file
|
|
output_path: Path for the output compressed file (None = auto-generate)
|
|
format: Compression format ('gzip', 'bz2', 'lzma', or 'zip')
|
|
remove_original: Whether to remove the original file after compression
|
|
|
|
Returns:
|
|
Path to the compressed file
|
|
|
|
Raises:
|
|
CompressionError: If compression fails
|
|
"""
|
|
input_path = Compression._ensure_path(input_path)
|
|
if not input_path.exists():
|
|
raise CompressionError("Input file does not exist")
|
|
|
|
Compression._validate_format(format)
|
|
|
|
if output_path is None:
|
|
ext = Compression.FORMAT_EXTENSION.get(format, f'.{format}')
|
|
output_path = input_path.with_suffix(input_path.suffix + ext)
|
|
else:
|
|
output_path = Compression._ensure_path(output_path)
|
|
|
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
try:
|
|
if format in Compression.DATA_FORMATS:
|
|
with open(input_path, 'rb') as src:
|
|
data = src.read()
|
|
compressed = Compression.compress_data(data, format)
|
|
with open(output_path, 'wb') as dst:
|
|
dst.write(compressed)
|
|
|
|
elif format == 'zip':
|
|
with zipfile.ZipFile(output_path, 'w', zipfile.ZIP_DEFLATED) as zf:
|
|
zf.write(input_path, arcname=input_path.name)
|
|
|
|
else:
|
|
raise CompressionError(f"Unsupported format: {format}")
|
|
|
|
except Exception as e:
|
|
raise CompressionError(f"File compression failed: {e}") from e
|
|
|
|
if remove_original:
|
|
try:
|
|
input_path.unlink()
|
|
except Exception as e:
|
|
raise CompressionError(f"Failed to remove original: {e}") from e
|
|
|
|
return output_path
|
|
|
|
@staticmethod
|
|
def create_archive(
|
|
files: List[PathLike],
|
|
output_path: PathLike,
|
|
format: str = 'zip'
|
|
) -> Path:
|
|
"""Create an archive from a list of files.
|
|
|
|
Args:
|
|
files: List of paths to files to include
|
|
output_path: Path to the resulting archive
|
|
format: Archive format ('zip', 'tar', 'tar.gz', etc.)
|
|
|
|
Returns:
|
|
Path to the created archive
|
|
|
|
Raises:
|
|
CompressionError: If archive creation fails
|
|
"""
|
|
output_path = Compression._ensure_path(output_path)
|
|
Compression._validate_format(format)
|
|
|
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
try:
|
|
if format == 'zip':
|
|
with zipfile.ZipFile(output_path, 'w', zipfile.ZIP_DEFLATED) as zf:
|
|
for f in files:
|
|
f = Compression._ensure_path(f)
|
|
if f.exists():
|
|
zf.write(f, arcname=f.name)
|
|
else:
|
|
raise CompressionError(f"File not found: {f}")
|
|
|
|
elif format in Compression.TAR_MODE_MAP:
|
|
mode = Compression.TAR_MODE_MAP[format]
|
|
with tarfile.open(output_path, mode) as tf:
|
|
for f in files:
|
|
f = Compression._ensure_path(f)
|
|
if f.exists():
|
|
tf.add(f, arcname=f.name)
|
|
else:
|
|
raise CompressionError(f"File not found: {f}")
|
|
else:
|
|
raise CompressionError(f"Unsupported format: {format}")
|
|
|
|
return output_path
|
|
|
|
except Exception as e:
|
|
if isinstance(e, CompressionError):
|
|
raise
|
|
raise CompressionError(f"Archive creation failed: {e}") from e
|
|
|
|
@staticmethod
|
|
def decompress_file(
|
|
input_path: PathLike,
|
|
output_path: Optional[PathLike] = None,
|
|
format: Optional[str] = None,
|
|
remove_compressed: bool = False
|
|
) -> Path:
|
|
"""Decompress a compressed file.
|
|
|
|
Args:
|
|
input_path: Path to the compressed file
|
|
output_path: Path for the output decompressed file (None = auto-generate)
|
|
format: Compression format (None = auto-detect from extension)
|
|
remove_compressed: Whether to remove the compressed file after decompression
|
|
|
|
Returns:
|
|
Path to the decompressed file
|
|
|
|
Raises:
|
|
CompressionError: If decompression fails
|
|
"""
|
|
input_path = Compression._ensure_path(input_path)
|
|
if not input_path.exists():
|
|
raise CompressionError("Input file does not exist")
|
|
|
|
if format is None:
|
|
format = Compression.EXTENSION_MAP.get(input_path.suffix.lower())
|
|
if not format:
|
|
raise CompressionError("Cannot auto-detect format")
|
|
|
|
Compression._validate_format(format)
|
|
|
|
if output_path is None:
|
|
output_path = input_path.with_suffix('')
|
|
else:
|
|
output_path = Compression._ensure_path(output_path)
|
|
|
|
try:
|
|
if format in Compression.DATA_FORMATS:
|
|
with open(input_path, 'rb') as src:
|
|
data = src.read()
|
|
decompressed = Compression.decompress_data(data, format)
|
|
with open(output_path, 'wb') as dst:
|
|
dst.write(decompressed)
|
|
|
|
elif format == 'zip':
|
|
with zipfile.ZipFile(input_path, 'r') as zf:
|
|
for name in zf.namelist():
|
|
Compression._validate_extraction_path(output_path.parent, name)
|
|
zf.extractall(output_path.parent)
|
|
|
|
else:
|
|
raise CompressionError(f"Unsupported format: {format}")
|
|
|
|
except Exception as e:
|
|
raise CompressionError(f"File decompression failed: {e}") from e
|
|
|
|
if remove_compressed:
|
|
input_path.unlink()
|
|
|
|
return output_path
|
|
|
|
@staticmethod
|
|
def extract_archive(
|
|
archive_path: PathLike,
|
|
output_dir: PathLike,
|
|
format: Optional[str] = None,
|
|
PASSWORD_REMOVED: Optional[bytes] = None
|
|
) -> List[Path]:
|
|
"""Extract archive (zip, tar, tar.gz, tar.bz2, tar.xz) to output directory.
|
|
|
|
Args:
|
|
archive_path: Path to the archive file
|
|
output_dir: Directory to extract to
|
|
format: Optional format hint ('zip', 'tar', 'tar.gz', etc.)
|
|
PASSWORD_REMOVED: Optional PASSWORD_REMOVED for encrypted archives
|
|
|
|
Returns:
|
|
List of extracted file paths
|
|
|
|
Raises:
|
|
CompressionError: If archive cannot be extracted
|
|
"""
|
|
archive_path = Compression._ensure_path(archive_path)
|
|
output_dir = Compression._ensure_path(output_dir)
|
|
|
|
if not archive_path.exists():
|
|
raise CompressionError("Archive not found")
|
|
|
|
# Auto-detect format if not provided
|
|
detected = format
|
|
if detected is None:
|
|
suffix = archive_path.suffix.lower()
|
|
if suffix == '.zip':
|
|
detected = 'zip'
|
|
elif archive_path.name.endswith('.tar.gz'):
|
|
detected = 'tar.gz'
|
|
elif archive_path.name.endswith('.tar.bz2'):
|
|
detected = 'tar.bz2'
|
|
elif archive_path.name.endswith('.tar.xz'):
|
|
detected = 'tar.xz'
|
|
elif suffix == '.tar':
|
|
detected = 'tar'
|
|
else:
|
|
raise CompressionError(f"Cannot auto-detect format for: {archive_path}")
|
|
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
extracted_files: List[Path] = []
|
|
|
|
try:
|
|
if detected == 'zip':
|
|
with zipfile.ZipFile(archive_path, 'r') as zf:
|
|
if PASSWORD_REMOVED:
|
|
zf.setPASSWORD_REMOVED(PASSWORD_REMOVED)
|
|
# Validate all paths before extracting
|
|
for name in zf.namelist():
|
|
Compression._validate_extraction_path(output_dir, name)
|
|
zf.extractall(output_dir)
|
|
extracted_files = [output_dir / name for name in zf.namelist()]
|
|
|
|
elif detected in Compression.TAR_MODE_MAP:
|
|
with tarfile.open(archive_path, 'r:*') as tf:
|
|
# Validate all paths before extracting
|
|
members = [m for m in tf.getmembers() if m.name not in ('.', './', '..', '../')]
|
|
for member in members:
|
|
Compression._validate_extraction_path(output_dir, member.name)
|
|
tf.extractall(output_dir, members=members)
|
|
extracted_files = [output_dir / member.name for member in members]
|
|
else:
|
|
raise CompressionError(f"Unsupported format: {detected}")
|
|
|
|
return extracted_files
|
|
|
|
except Exception as e:
|
|
if isinstance(e, CompressionError):
|
|
raise
|
|
raise CompressionError(f"Archive extraction failed: {e}") from e
|
|
|
|
@staticmethod
|
|
def get_compression_ratio(original: int, compressed: int) -> float:
|
|
"""Calculate compression ratio.
|
|
|
|
Args:
|
|
original: Original (uncompressed) size in bytes
|
|
compressed: Compressed size in bytes
|
|
|
|
Returns:
|
|
Compression ratio (original / compressed), or 0 if compressed is 0
|
|
"""
|
|
return original / compressed if compressed else 0.0
|
|
|
|
@staticmethod
|
|
def estimate_compressed_size(
|
|
size: int,
|
|
format: str = 'gzip',
|
|
data_type: str = 'text'
|
|
) -> int:
|
|
"""Estimate compressed size based on historical ratios.
|
|
|
|
Args:
|
|
size: Original size in bytes
|
|
format: Compression format
|
|
data_type: Type of data ('text', 'binary', 'image', or 'video')
|
|
|
|
Returns:
|
|
Estimated compressed size in bytes
|
|
"""
|
|
format_key = format.replace('tar.', '')
|
|
ratios = Compression.COMPRESSION_RATIOS.get(format_key)
|
|
if ratios:
|
|
ratio = ratios.get(data_type, 0.5)
|
|
else:
|
|
ratio = 0.5
|
|
|
|
return int(size * ratio)
|