| 1 | """ |
|---|
| 2 | Ported to Python 3. |
|---|
| 3 | """ |
|---|
| 4 | |
|---|
| 5 | import struct |
|---|
| 6 | |
|---|
| 7 | import attr |
|---|
| 8 | |
|---|
| 9 | from ..util.hashutil import ( |
|---|
| 10 | tagged_hash, |
|---|
| 11 | ) |
|---|
| 12 | from .lease import ( |
|---|
| 13 | LeaseInfo, |
|---|
| 14 | ) |
|---|
| 15 | from .lease_schema import ( |
|---|
| 16 | v1_mutable, |
|---|
| 17 | v2_mutable, |
|---|
| 18 | ) |
|---|
| 19 | |
|---|
| 20 | def _magic(version): |
|---|
| 21 | # type: (int) -> bytes |
|---|
| 22 | """ |
|---|
| 23 | Compute a "magic" header string for a container of the given version. |
|---|
| 24 | |
|---|
| 25 | :param version: The version number of the container. |
|---|
| 26 | """ |
|---|
| 27 | # Make it easy for people to recognize |
|---|
| 28 | human_readable = u"Tahoe mutable container v{:d}\n".format(version).encode("ascii") |
|---|
| 29 | # But also keep the chance of accidental collision low |
|---|
| 30 | if version == 1: |
|---|
| 31 | # It's unclear where this byte sequence came from. It may have just |
|---|
| 32 | # been random. In any case, preserve it since it is the magic marker |
|---|
| 33 | # in all v1 share files. |
|---|
| 34 | random_bytes = b"\x75\x09\x44\x03\x8e" |
|---|
| 35 | else: |
|---|
| 36 | # For future versions, use a reproducable scheme. |
|---|
| 37 | random_bytes = tagged_hash( |
|---|
| 38 | b"allmydata_mutable_container_header", |
|---|
| 39 | human_readable, |
|---|
| 40 | truncate_to=5, |
|---|
| 41 | ) |
|---|
| 42 | magic = human_readable + random_bytes |
|---|
| 43 | assert len(magic) == 32 |
|---|
| 44 | if version > 1: |
|---|
| 45 | # The chance of collision is pretty low but let's just be sure about |
|---|
| 46 | # it. |
|---|
| 47 | assert magic != _magic(version - 1) |
|---|
| 48 | |
|---|
| 49 | return magic |
|---|
| 50 | |
|---|
| 51 | def _header(magic, extra_lease_offset, nodeid, write_enabler): |
|---|
| 52 | # type: (bytes, int, bytes, bytes) -> bytes |
|---|
| 53 | """ |
|---|
| 54 | Construct a container header. |
|---|
| 55 | |
|---|
| 56 | :param nodeid: A unique identifier for the node holding this |
|---|
| 57 | container. |
|---|
| 58 | |
|---|
| 59 | :param write_enabler: A secret shared with the client used to |
|---|
| 60 | authorize changes to the contents of this container. |
|---|
| 61 | """ |
|---|
| 62 | fixed_header = struct.pack( |
|---|
| 63 | ">32s20s32sQQ", |
|---|
| 64 | magic, |
|---|
| 65 | nodeid, |
|---|
| 66 | write_enabler, |
|---|
| 67 | # data length, initially the container is empty |
|---|
| 68 | 0, |
|---|
| 69 | extra_lease_offset, |
|---|
| 70 | ) |
|---|
| 71 | blank_leases = b"\x00" * LeaseInfo().mutable_size() * 4 |
|---|
| 72 | extra_lease_count = struct.pack(">L", 0) |
|---|
| 73 | |
|---|
| 74 | return b"".join([ |
|---|
| 75 | fixed_header, |
|---|
| 76 | # share data will go in between the next two items eventually but |
|---|
| 77 | # for now there is none. |
|---|
| 78 | blank_leases, |
|---|
| 79 | extra_lease_count, |
|---|
| 80 | ]) |
|---|
| 81 | |
|---|
| 82 | |
|---|
| 83 | _HEADER_FORMAT = ">32s20s32sQQ" |
|---|
| 84 | |
|---|
| 85 | # This size excludes leases |
|---|
| 86 | _HEADER_SIZE = struct.calcsize(_HEADER_FORMAT) |
|---|
| 87 | |
|---|
| 88 | _EXTRA_LEASE_OFFSET = _HEADER_SIZE + 4 * LeaseInfo().mutable_size() |
|---|
| 89 | |
|---|
| 90 | |
|---|
| 91 | @attr.s(frozen=True) |
|---|
| 92 | class _Schema: |
|---|
| 93 | """ |
|---|
| 94 | Implement encoding and decoding for the mutable container. |
|---|
| 95 | |
|---|
| 96 | :ivar int version: the version number of the schema this object supports |
|---|
| 97 | |
|---|
| 98 | :ivar lease_serializer: an object that is responsible for lease |
|---|
| 99 | serialization and unserialization |
|---|
| 100 | """ |
|---|
| 101 | version = attr.ib() |
|---|
| 102 | lease_serializer = attr.ib() |
|---|
| 103 | _magic = attr.ib() |
|---|
| 104 | |
|---|
| 105 | @classmethod |
|---|
| 106 | def for_version(cls, version, lease_serializer): |
|---|
| 107 | return cls(version, lease_serializer, magic=_magic(version)) |
|---|
| 108 | |
|---|
| 109 | def magic_matches(self, candidate_magic): |
|---|
| 110 | # type: (bytes) -> bool |
|---|
| 111 | """ |
|---|
| 112 | Return ``True`` if a candidate string matches the expected magic string |
|---|
| 113 | from a mutable container header, ``False`` otherwise. |
|---|
| 114 | """ |
|---|
| 115 | return candidate_magic[:len(self._magic)] == self._magic |
|---|
| 116 | |
|---|
| 117 | def header(self, nodeid, write_enabler): |
|---|
| 118 | return _header(self._magic, _EXTRA_LEASE_OFFSET, nodeid, write_enabler) |
|---|
| 119 | |
|---|
| 120 | ALL_SCHEMAS = { |
|---|
| 121 | _Schema.for_version(version=2, lease_serializer=v2_mutable), |
|---|
| 122 | _Schema.for_version(version=1, lease_serializer=v1_mutable), |
|---|
| 123 | } |
|---|
| 124 | ALL_SCHEMA_VERSIONS = {schema.version for schema in ALL_SCHEMAS} |
|---|
| 125 | NEWEST_SCHEMA_VERSION = max(ALL_SCHEMAS, key=lambda schema: schema.version) |
|---|
| 126 | |
|---|
| 127 | def schema_from_header(header): |
|---|
| 128 | # (int) -> Optional[type] |
|---|
| 129 | """ |
|---|
| 130 | Find the schema object that corresponds to a certain version number. |
|---|
| 131 | """ |
|---|
| 132 | for schema in ALL_SCHEMAS: |
|---|
| 133 | if schema.magic_matches(header): |
|---|
| 134 | return schema |
|---|
| 135 | return None |
|---|