Source code for terselib.filetype

"""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"