2022-12-22 17:13:12 +00:00
|
|
|
"""
|
|
|
|
A module that loads pre-generated test vectors.
|
|
|
|
|
2023-01-04 00:22:38 +00:00
|
|
|
:ivar DATA_PATH: The path of the file containing test vectors.
|
2022-12-22 17:13:12 +00:00
|
|
|
|
2023-01-16 20:53:24 +00:00
|
|
|
:ivar capabilities: The capability test vectors.
|
2022-12-22 17:13:12 +00:00
|
|
|
"""
|
|
|
|
|
2023-01-04 00:22:38 +00:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
from typing import TextIO
|
|
|
|
from attrs import frozen
|
2022-12-22 15:51:59 +00:00
|
|
|
from yaml import safe_load
|
2023-01-04 00:22:38 +00:00
|
|
|
from base64 import b64encode, b64decode
|
2022-12-22 15:51:59 +00:00
|
|
|
|
2023-01-12 22:27:37 +00:00
|
|
|
from twisted.python.filepath import FilePath
|
|
|
|
|
2023-01-17 13:41:10 +00:00
|
|
|
from ..util import CHK, SSK
|
2023-01-16 20:53:24 +00:00
|
|
|
|
2023-01-17 13:45:38 +00:00
|
|
|
DATA_PATH: FilePath = FilePath(__file__).sibling("test_vectors.yaml")
|
2023-01-16 20:53:24 +00:00
|
|
|
|
|
|
|
# The version of the persisted test vector data this code can interpret.
|
2023-01-17 13:45:38 +00:00
|
|
|
CURRENT_VERSION: str = "2023-01-16.2"
|
2022-12-22 15:51:59 +00:00
|
|
|
|
2023-01-04 00:22:38 +00:00
|
|
|
@frozen
|
|
|
|
class Sample:
|
|
|
|
"""
|
|
|
|
Some instructions for building a long byte string.
|
|
|
|
|
|
|
|
:ivar seed: Some bytes to repeat some times to produce the string.
|
|
|
|
:ivar length: The length of the desired byte string.
|
|
|
|
"""
|
|
|
|
seed: bytes
|
|
|
|
length: int
|
|
|
|
|
|
|
|
@frozen
|
|
|
|
class Param:
|
|
|
|
"""
|
|
|
|
Some ZFEC parameters.
|
|
|
|
"""
|
|
|
|
required: int
|
|
|
|
total: int
|
|
|
|
|
|
|
|
# CHK have a max of 256 shares. SDMF / MDMF have a max of 255 shares!
|
|
|
|
# Represent max symbolically and resolve it when we know what format we're
|
|
|
|
# dealing with.
|
|
|
|
MAX_SHARES = "max"
|
|
|
|
|
|
|
|
@frozen
|
|
|
|
class SeedParam:
|
|
|
|
"""
|
|
|
|
Some ZFEC parameters, almost.
|
|
|
|
|
|
|
|
:ivar required: The number of required shares.
|
|
|
|
|
|
|
|
:ivar total: Either the number of total shares or the constant
|
|
|
|
``MAX_SHARES`` to indicate that the total number of shares should be
|
|
|
|
the maximum number supported by the object format.
|
|
|
|
"""
|
|
|
|
required: int
|
|
|
|
total: int | str
|
|
|
|
|
|
|
|
def realize(self, max_total: int) -> Param:
|
|
|
|
"""
|
|
|
|
Create a ``Param`` from this object's values, possibly
|
|
|
|
substituting the given real value for total if necessary.
|
|
|
|
|
|
|
|
:param max_total: The value to use to replace ``MAX_SHARES`` if
|
|
|
|
necessary.
|
|
|
|
"""
|
|
|
|
if self.total == MAX_SHARES:
|
|
|
|
return Param(self.required, max_total)
|
|
|
|
return Param(self.required, self.total)
|
|
|
|
|
|
|
|
@frozen
|
|
|
|
class Case:
|
|
|
|
"""
|
|
|
|
Represent one case for which we want/have a test vector.
|
|
|
|
"""
|
|
|
|
seed_params: Param
|
|
|
|
convergence: bytes
|
|
|
|
seed_data: Sample
|
2023-01-16 20:53:24 +00:00
|
|
|
fmt: CHK | SSK
|
2023-01-04 00:22:38 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def data(self):
|
|
|
|
return stretch(self.seed_data.seed, self.seed_data.length)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def params(self):
|
2023-01-16 20:53:24 +00:00
|
|
|
return self.seed_params.realize(self.fmt.max_shares)
|
2023-01-04 00:22:38 +00:00
|
|
|
|
|
|
|
|
|
|
|
def encode_bytes(b: bytes) -> str:
|
|
|
|
"""
|
|
|
|
Base64 encode some bytes to text so they are representable in JSON.
|
|
|
|
"""
|
|
|
|
return b64encode(b).decode("ascii")
|
|
|
|
|
|
|
|
|
|
|
|
def decode_bytes(b: str) -> bytes:
|
|
|
|
"""
|
|
|
|
Base64 decode some text to bytes.
|
|
|
|
"""
|
|
|
|
return b64decode(b.encode("ascii"))
|
|
|
|
|
|
|
|
|
|
|
|
def stretch(seed: bytes, size: int) -> bytes:
|
|
|
|
"""
|
|
|
|
Given a simple description of a byte string, return the byte string
|
|
|
|
itself.
|
|
|
|
"""
|
|
|
|
assert isinstance(seed, bytes)
|
|
|
|
assert isinstance(size, int)
|
|
|
|
assert size > 0
|
|
|
|
assert len(seed) > 0
|
|
|
|
|
|
|
|
multiples = size // len(seed) + 1
|
|
|
|
return (seed * multiples)[:size]
|
|
|
|
|
|
|
|
|
2023-01-16 20:53:24 +00:00
|
|
|
def load_format(serialized: dict) -> CHK | SSK:
|
|
|
|
if serialized["kind"] == "chk":
|
|
|
|
return CHK.load(serialized["params"])
|
|
|
|
elif serialized["kind"] == "ssk":
|
|
|
|
return SSK.load(serialized["params"])
|
|
|
|
else:
|
|
|
|
raise ValueError(f"Unrecognized format: {serialized}")
|
|
|
|
|
|
|
|
|
2023-01-04 00:22:38 +00:00
|
|
|
def load_capabilities(f: TextIO) -> dict[Case, str]:
|
|
|
|
data = safe_load(f)
|
2023-01-12 20:19:01 +00:00
|
|
|
if data is None:
|
|
|
|
return {}
|
2023-01-16 20:53:24 +00:00
|
|
|
if data["version"] != CURRENT_VERSION:
|
|
|
|
print(
|
|
|
|
f"Current version is {CURRENT_VERSION}; "
|
2023-01-17 13:45:38 +00:00
|
|
|
f"cannot load version {data['version']} data."
|
2023-01-16 20:53:24 +00:00
|
|
|
)
|
|
|
|
return {}
|
|
|
|
|
2023-01-04 00:22:38 +00:00
|
|
|
return {
|
|
|
|
Case(
|
|
|
|
seed_params=SeedParam(case["zfec"]["required"], case["zfec"]["total"]),
|
|
|
|
convergence=decode_bytes(case["convergence"]),
|
|
|
|
seed_data=Sample(decode_bytes(case["sample"]["seed"]), case["sample"]["length"]),
|
2023-01-16 20:53:24 +00:00
|
|
|
fmt=load_format(case["format"]),
|
2023-01-04 00:22:38 +00:00
|
|
|
): case["expected"]
|
|
|
|
for case
|
|
|
|
in data["vector"]
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2022-12-26 17:06:34 +00:00
|
|
|
try:
|
2022-12-27 14:12:34 +00:00
|
|
|
with DATA_PATH.open() as f:
|
2023-01-04 00:22:38 +00:00
|
|
|
capabilities: dict[Case, str] = load_capabilities(f)
|
2022-12-26 17:06:34 +00:00
|
|
|
except FileNotFoundError:
|
2022-12-27 14:12:34 +00:00
|
|
|
capabilities = {}
|