|
1 | 1 | from dataclasses import fields, is_dataclass |
2 | 2 | import io |
3 | | -from typing import BinaryIO, Dict, Optional, TypeVar, Type, Union as PyUnion, get_args, get_origin |
| 3 | +from typing import BinaryIO, Dict, List as PyList, Optional, TypeVar, Type, Union as PyUnion, \ |
| 4 | + get_args, get_origin |
4 | 5 | from textwrap import indent |
5 | 6 | from remerkleable.bitfields import Bitvector |
6 | | -from remerkleable.complex import ComplexView, encode_offset |
| 7 | +from remerkleable.complex import ComplexView, FieldOffset, decode_offset, encode_offset |
7 | 8 | from remerkleable.core import View, ViewHook, OFFSET_BYTE_LENGTH |
8 | 9 | from remerkleable.tree import NavigationError, Node, PairNode, \ |
9 | 10 | get_depth, subtree_fill_to_contents, zero_node |
10 | 11 |
|
11 | 12 | T = TypeVar('T') |
12 | 13 | N = TypeVar('N') |
| 14 | +P = TypeVar('P', bound="PartialContainer") |
13 | 15 |
|
14 | 16 | PartialFields = Dict[str, tuple[Type[View], bool]] |
15 | 17 |
|
@@ -86,6 +88,30 @@ def fields(cls) -> PartialFields: |
86 | 88 | ret[fkey] = (ftyp, fopt) |
87 | 89 | return ret |
88 | 90 |
|
| 91 | + @classmethod |
| 92 | + def is_fixed_byte_length(cls) -> bool: |
| 93 | + return False |
| 94 | + |
| 95 | + @classmethod |
| 96 | + def min_byte_length(cls) -> int: |
| 97 | + total = Bitvector[cls.N].type_byte_length() |
| 98 | + for _, (ftyp, fopt) in cls.fields().items(): |
| 99 | + if fopt: |
| 100 | + continue |
| 101 | + if not ftyp.is_fixed_byte_length(): |
| 102 | + total += OFFSET_BYTE_LENGTH |
| 103 | + total += ftyp.min_byte_length() |
| 104 | + return total |
| 105 | + |
| 106 | + @classmethod |
| 107 | + def max_byte_length(cls) -> int: |
| 108 | + total = Bitvector[cls.N].type_byte_length() |
| 109 | + for _, (ftyp, _) in cls.fields().items(): |
| 110 | + if not ftyp.is_fixed_byte_length(): |
| 111 | + total += OFFSET_BYTE_LENGTH |
| 112 | + total += ftyp.max_byte_length() |
| 113 | + return total |
| 114 | + |
89 | 115 | def active_fields(self) -> Bitvector: |
90 | 116 | active_fields_node = super().get_backing().get_right() |
91 | 117 | return Bitvector[self.__class__.N].view_from_backing(active_fields_node) |
@@ -129,6 +155,52 @@ def __repr__(self): |
129 | 155 | def type_repr(cls) -> str: |
130 | 156 | return f"PartialContainer[{cls.T.__name__}, {cls.N}]" |
131 | 157 |
|
| 158 | + @classmethod |
| 159 | + def deserialize(cls: Type[P], stream: BinaryIO, scope: int) -> P: |
| 160 | + num_prefix_bytes = Bitvector[cls.N].type_byte_length() |
| 161 | + if scope < num_prefix_bytes: |
| 162 | + raise ValueError("scope too small, cannot read PartialContainer active fields") |
| 163 | + active_fields = Bitvector[cls.N].deserialize(stream, num_prefix_bytes) |
| 164 | + scope = scope - num_prefix_bytes |
| 165 | + |
| 166 | + max_findex = 0 |
| 167 | + field_values: Dict[str, Optional[View]] = {} |
| 168 | + dyn_fields: PyList[FieldOffset] = [] |
| 169 | + fixed_size = 0 |
| 170 | + for findex, (fkey, (ftyp, _)) in enumerate(cls.fields().items()): |
| 171 | + max_findex = findex |
| 172 | + if not active_fields.get(findex): |
| 173 | + field_values[fkey] = None |
| 174 | + continue |
| 175 | + if ftyp.is_fixed_byte_length(): |
| 176 | + fsize = ftyp.type_byte_length() |
| 177 | + field_values[fkey] = ftyp.deserialize(stream, fsize) |
| 178 | + fixed_size += fsize |
| 179 | + else: |
| 180 | + dyn_fields.append(FieldOffset( |
| 181 | + key=fkey, typ=ftyp, offset=int(decode_offset(stream)))) |
| 182 | + fixed_size += OFFSET_BYTE_LENGTH |
| 183 | + if len(dyn_fields) > 0: |
| 184 | + if dyn_fields[0].offset < fixed_size: |
| 185 | + raise Exception(f"first offset {dyn_fields[0].offset} is " |
| 186 | + f"smaller than expected fixed size {fixed_size}") |
| 187 | + for i, (fkey, ftyp, foffset) in enumerate(dyn_fields): |
| 188 | + next_offset = dyn_fields[i + 1].offset if i + 1 < len(dyn_fields) else scope |
| 189 | + if foffset > next_offset: |
| 190 | + raise Exception(f"offset {i} is invalid: {foffset} " |
| 191 | + f"larger than next offset {next_offset}") |
| 192 | + fsize = next_offset - foffset |
| 193 | + f_min_size, f_max_size = ftyp.min_byte_length(), ftyp.max_byte_length() |
| 194 | + if not (f_min_size <= fsize <= f_max_size): |
| 195 | + raise Exception(f"offset {i} is invalid, size out of bounds: " |
| 196 | + f"{foffset}, next {next_offset}, implied size: {fsize}, " |
| 197 | + f"size bounds: [{f_min_size}, {f_max_size}]") |
| 198 | + field_values[fkey] = ftyp.deserialize(stream, fsize) |
| 199 | + for findex in range(max_findex + 1, cls.N): |
| 200 | + if active_fields.get(findex): |
| 201 | + raise Exception(f"unknown field index {findex}") |
| 202 | + return cls(**field_values) # type: ignore |
| 203 | + |
132 | 204 | def serialize(self, stream: BinaryIO) -> int: |
133 | 205 | active_fields = self.active_fields() |
134 | 206 | num_prefix_bytes = active_fields.serialize(stream) |
|
0 commit comments