mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-09-26 21:07:56 +00:00
71 lines
2.8 KiB
Python
71 lines
2.8 KiB
Python
"""Pinned Google Zopfli helpers for static and host-generated DEFLATE data.
|
|
|
|
Zopfli writes standards-compliant DEFLATE streams, so the existing device
|
|
decoders need no additional firmware code or RAM. It is deliberately used
|
|
only where compression happens on a development machine or host controller:
|
|
firmware assets, test vectors, and BLE mOTA transport blocks.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
ZOPFLI_VERSION = "0.4.3"
|
|
|
|
try:
|
|
import zopfli
|
|
from zopfli import gzip as _gzip
|
|
from zopfli import zlib as _zlib
|
|
except ImportError as error:
|
|
raise RuntimeError(
|
|
"Google Zopfli 0.4.3 is required to generate MeshCore compressed "
|
|
"assets. Install it with 'python -m pip install -r "
|
|
"requirements-build.txt'."
|
|
) from error
|
|
|
|
if zopfli.__version__ != ZOPFLI_VERSION:
|
|
raise RuntimeError(
|
|
"MeshCore requires Google Zopfli %s, but %s is installed. Run "
|
|
"'python -m pip install -r requirements-build.txt'."
|
|
% (ZOPFLI_VERSION, zopfli.__version__)
|
|
)
|
|
|
|
# Above zlib's maximum level 9: Zopfli refines the LZ77 model repeatedly.
|
|
# Fifteen iterations is the upstream binding's practical default for assets
|
|
# of this size and keeps CI/build times reasonable.
|
|
ITERATIONS = 15
|
|
# Zopfli has no finite algorithmic iteration ceiling. Its ``--i1000``
|
|
# convention is the maximum-effort setting used for host-cached mOTA transfer
|
|
# blocks, where the extra host CPU time is preferable to wasting radio airtime.
|
|
MAXIMUM_ITERATIONS = 1000
|
|
|
|
|
|
def _options(numiterations: int = ITERATIONS) -> dict[str, int | bool]:
|
|
if numiterations < 1:
|
|
raise ValueError("Zopfli requires at least one iteration")
|
|
return {
|
|
"numiterations": numiterations,
|
|
"blocksplitting": True,
|
|
"blocksplittinglast": False,
|
|
"blocksplittingmax": 15,
|
|
}
|
|
|
|
|
|
def gzip_compress(data: bytes) -> bytes:
|
|
"""Compress *data* as deterministic RFC 1952 gzip using Google Zopfli."""
|
|
encoded = bytearray(_gzip.compress(data, **_options()))
|
|
# The gzip OS byte is informational. Normalize it to RFC 1952's unknown
|
|
# value so a checked-in asset is identical across operating systems.
|
|
if len(encoded) < 10 or encoded[:3] != b"\x1f\x8b\x08":
|
|
raise RuntimeError("Zopfli returned an invalid gzip stream")
|
|
encoded[9] = 0xFF
|
|
return bytes(encoded)
|
|
|
|
|
|
def raw_deflate_compress(data: bytes, *, numiterations: int = ITERATIONS) -> bytes:
|
|
"""Compress *data* to an RFC 1951 raw-DEFLATE stream with Zopfli."""
|
|
encoded = _zlib.compress(data, **_options(numiterations))
|
|
# A zlib wrapper is exactly a two-byte header and four-byte Adler-32
|
|
# trailer. Removing it preserves the RFC 1951 body expected by tinf.
|
|
if len(encoded) < 6 or (encoded[0] & 0x0F) != 8:
|
|
raise RuntimeError("Zopfli returned an invalid zlib stream")
|
|
return encoded[2:-4]
|