tahoe-lafs/integration/vectors/vectors.py

156 lines
4.2 KiB
Python
Raw Normal View History

2022-12-22 17:13:12 +00:00
"""
A module that loads pre-generated test vectors.
: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
"""
from __future__ import annotations
from typing import TextIO
from attrs import frozen
from yaml import safe_load, safe_dump
from base64 import b64encode, b64decode
from twisted.python.filepath import FilePath
from .model import Param, Sample, SeedParam
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"
@frozen
class Case:
"""
Represent one case for which we want/have a test vector.
"""
seed_params: Param
segment_size: int
convergence: bytes
seed_data: Sample
2023-01-16 20:53:24 +00:00
fmt: CHK | SSK
@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)
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]
def save_capabilities(results: list[tuple[Case, str]], path: FilePath = DATA_PATH) -> None:
"""
Save some test vector cases and their expected values.
This is logically the inverse of ``load_capabilities``.
"""
path.setContent(safe_dump({
"version": CURRENT_VERSION,
"vector": [
{
"convergence": encode_bytes(case.convergence),
"format": {
"kind": case.fmt.kind,
"params": case.fmt.to_json(),
},
"sample": {
"seed": encode_bytes(case.seed_data.seed),
"length": case.seed_data.length,
},
"zfec": {
"segmentSize": case.segment_size,
"required": case.params.required,
"total": case.params.total,
},
"expected": cap,
}
for (case, cap)
in results
],
}).encode("ascii"))
2023-01-16 20:53:24 +00:00
def load_format(serialized: dict) -> CHK | SSK:
"""
Load an encrypted object format from a simple description of it.
:param serialized: A ``dict`` describing either CHK or SSK, possibly with
some parameters.
"""
2023-01-16 20:53:24 +00:00
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}")
def load_capabilities(f: TextIO) -> dict[Case, str]:
"""
Load some test vector cases and their expected results from the given
file.
This is logically the inverse of ``save_capabilities``.
"""
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 {}
return {
Case(
seed_params=SeedParam(case["zfec"]["required"], case["zfec"]["total"]),
segment_size=case["zfec"]["segmentSize"],
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"]),
): case["expected"]
for case
in data["vector"]
}
try:
with DATA_PATH.open() as f:
capabilities: dict[Case, str] = load_capabilities(f)
except FileNotFoundError:
capabilities = {}