.beb and .json serialization of the document
This commit is contained in:
75
src/bedit_core/serialization/beb_codec.py
Normal file
75
src/bedit_core/serialization/beb_codec.py
Normal file
@@ -0,0 +1,75 @@
|
||||
"""Versioned binary BEB codec.
|
||||
|
||||
The uncompressed header contains the magic bytes and codec version, allowing
|
||||
the correct decoder to be selected even if later versions change compression
|
||||
or serialization methods.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import zlib
|
||||
from pathlib import Path
|
||||
from typing import Any, Mapping
|
||||
|
||||
import msgpack
|
||||
|
||||
FORMAT_NAME = "beb"
|
||||
FILE_FORMAT_VERSION = 1
|
||||
BEB_MAGIC = b"BEB\x00"
|
||||
_VERSION_SIZE = 4
|
||||
|
||||
|
||||
def load_data(path: Path) -> tuple[int, Mapping[str, Any]]:
|
||||
"""Read a BEB header and decode its raw document mapping.
|
||||
|
||||
An unknown version is returned with an empty mapping so the migration
|
||||
layer can produce the standard unsupported-version error without trying
|
||||
an incompatible decoder.
|
||||
"""
|
||||
try:
|
||||
payload = path.read_bytes()
|
||||
if not payload.startswith(BEB_MAGIC):
|
||||
raise ValueError("missing BEB file header")
|
||||
header_end = len(BEB_MAGIC) + _VERSION_SIZE
|
||||
if len(payload) < header_end:
|
||||
raise ValueError("truncated BEB file header")
|
||||
version = int.from_bytes(payload[len(BEB_MAGIC) : header_end], "big")
|
||||
encoded = payload[header_end:]
|
||||
decoder = _DECODERS.get(version)
|
||||
if decoder is None:
|
||||
# The version can be inspected without trying to decompress or
|
||||
# deserialize using the wrong algorithm.
|
||||
return version, {}
|
||||
data = decoder(encoded)
|
||||
except (OSError, ValueError, zlib.error, msgpack.exceptions.MsgpackException) as exc:
|
||||
raise ValueError(f"could not read BEB document {path}: {exc}") from exc
|
||||
if not isinstance(data, Mapping):
|
||||
raise ValueError(f"BEB document {path} must contain a map at its root")
|
||||
return version, data
|
||||
|
||||
|
||||
def save_data(data: Mapping[str, Any], path: Path) -> None:
|
||||
"""Encode raw document data using the current BEB version and write it."""
|
||||
encoded = _encode_v1(data)
|
||||
version = FILE_FORMAT_VERSION.to_bytes(_VERSION_SIZE, "big")
|
||||
path.write_bytes(BEB_MAGIC + version + encoded)
|
||||
|
||||
|
||||
def _encode_v1(data: Mapping[str, Any]) -> bytes:
|
||||
"""Encode BEB version 1 as zlib-compressed MessagePack."""
|
||||
return zlib.compress(msgpack.packb(dict(data), use_bin_type=True))
|
||||
|
||||
|
||||
def _decode_v1(payload: bytes) -> Any:
|
||||
"""Decode a zlib-compressed MessagePack BEB version 1 payload."""
|
||||
return msgpack.unpackb(
|
||||
zlib.decompress(payload),
|
||||
raw=False,
|
||||
strict_map_key=False,
|
||||
)
|
||||
|
||||
|
||||
# Keep old decoders when adding a new BEB encoding version.
|
||||
_DECODERS = {
|
||||
1: _decode_v1,
|
||||
}
|
||||
Reference in New Issue
Block a user