"""File-type detection for decompressed output.
When a TERSE file is decompressed in binary mode the original file name
and type are not recorded in the container, so :func:`detect_extension`
sniffs the decompressed bytes to pick a sensible file extension. It uses
the :mod:`magic` package (libmagic bindings) when available and falls back
to a small built-in signature table otherwise.
"""
from __future__ import annotations
import logging
import mimetypes
logger = logging.getLogger(__name__)
try:
import magic as _magic
HAVE_MAGIC = True
except ImportError: # pragma: no cover - depends on environment
_magic = None
HAVE_MAGIC = False
#: Fallback signature table used when python-magic is unavailable:
#: (offset, signature bytes, extension).
_SIGNATURES = (
(0, b"\x1f\x8b", ".gz"),
(0, b"PK\x03\x04", ".zip"),
(0, b"PK\x05\x06", ".zip"),
(0, b"%PDF-", ".pdf"),
(0, b"\x89PNG\r\n\x1a\n", ".png"),
(0, b"\xff\xd8\xff", ".jpg"),
(0, b"GIF8", ".gif"),
(0, b"BZh", ".bz2"),
(0, b"\xfd7zXZ\x00", ".xz"),
(0, b"7z\xbc\xaf\x27\x1c", ".7z"),
(0, b"\x7fELF", ".elf"),
(0, b"MZ", ".exe"),
(0, b"SQLite format 3\x00", ".sqlite"),
(0, b"\xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1", ".xls"),
(0, b"\x09\x00\x04\x00", ".xls"), # BIFF2 worksheet BOF record
(0, b"\x09\x02\x06\x00", ".xls"), # BIFF3
(0, b"\x09\x04\x06\x00", ".xls"), # BIFF4
(0, b"OggS", ".ogg"),
(0, b"ID3", ".mp3"),
(0, b"\x1a\x45\xdf\xa3", ".mkv"),
(4, b"ftyp", ".mp4"),
(257, b"ustar", ".tar"),
(0, b"CA\x01", ".xmi"), # z/OS TSO TRANSMIT (XMIT) begins INMR01
)
#: Overrides for MIME types whose mimetypes-guessed extension is poor.
_MIME_OVERRIDES = {
"application/octet-stream": ".bin",
"text/plain": ".txt",
"application/x-tar": ".tar",
"application/gzip": ".gz",
"application/zip": ".zip",
"application/vnd.ms-excel": ".xls",
}
def _looks_like_text(sample: bytes) -> bool:
"""Heuristic: True when *sample* contains no NULs and is mostly
printable ASCII."""
if not sample:
return True
if b"\x00" in sample:
return False
printable = sum(1 for b in sample if 0x20 <= b < 0x7F or b in (9, 10, 13))
return printable / len(sample) > 0.95
[docs]
def detect_extension(data: bytes, default: str = ".bin") -> str:
"""Guess an appropriate file extension for decompressed bytes.
:param data: The decompressed output (only the first 8 KiB are
examined, plus the tar signature window).
:param default: Extension returned when nothing better is known.
:returns: An extension including the leading dot (e.g. ``".xls"``).
"""
sample = bytes(data[:8192])
if HAVE_MAGIC:
try:
mime = _magic.from_buffer(sample, mime=True)
logger.debug("libmagic reports MIME type %s", mime)
# octet-stream is libmagic's "don't know" — give the signature
# table below a chance before settling for .bin
if mime != "application/octet-stream":
if mime in _MIME_OVERRIDES:
return _MIME_OVERRIDES[mime]
ext = mimetypes.guess_extension(mime)
if ext:
return ext
except Exception as exc: # pragma: no cover - libmagic hiccups
logger.debug("libmagic detection failed: %s", exc)
for offset, sig, ext in _SIGNATURES:
if data[offset:offset + len(sig)] == sig:
return ext
if _looks_like_text(sample):
return ".txt"
return default
[docs]
def describe(data: bytes) -> str:
"""Return a human-readable description of the detected content type.
:param data: The decompressed output bytes.
:returns: A description string such as ``"Zip archive data"``; falls
back to a generic label when libmagic is unavailable.
"""
if HAVE_MAGIC:
try:
return _magic.from_buffer(bytes(data[:8192]))
except Exception: # pragma: no cover
pass
return "text" if _looks_like_text(bytes(data[:8192])) else "binary data"