otdf-python 0.1.10__py3-none-any.whl → 0.3.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. otdf_python/__init__.py +25 -0
  2. otdf_python/__main__.py +12 -0
  3. otdf_python/address_normalizer.py +84 -0
  4. otdf_python/aesgcm.py +55 -0
  5. otdf_python/assertion_config.py +84 -0
  6. otdf_python/asym_crypto.py +198 -0
  7. otdf_python/auth_headers.py +33 -0
  8. otdf_python/autoconfigure_utils.py +113 -0
  9. otdf_python/cli.py +569 -0
  10. otdf_python/collection_store.py +41 -0
  11. otdf_python/collection_store_impl.py +22 -0
  12. otdf_python/config.py +69 -0
  13. otdf_python/connect_client.py +0 -0
  14. otdf_python/constants.py +1 -0
  15. otdf_python/crypto_utils.py +78 -0
  16. otdf_python/dpop.py +81 -0
  17. otdf_python/ecc_constants.py +176 -0
  18. otdf_python/ecc_mode.py +83 -0
  19. otdf_python/ecdh.py +317 -0
  20. otdf_python/eckeypair.py +75 -0
  21. otdf_python/header.py +181 -0
  22. otdf_python/invalid_zip_exception.py +8 -0
  23. otdf_python/kas_client.py +709 -0
  24. otdf_python/kas_connect_rpc_client.py +213 -0
  25. otdf_python/kas_info.py +25 -0
  26. otdf_python/kas_key_cache.py +52 -0
  27. otdf_python/key_type.py +31 -0
  28. otdf_python/key_type_constants.py +43 -0
  29. otdf_python/manifest.py +215 -0
  30. otdf_python/nanotdf.py +863 -0
  31. otdf_python/nanotdf_ecdsa_struct.py +132 -0
  32. otdf_python/nanotdf_type.py +43 -0
  33. otdf_python/policy_binding_serializer.py +39 -0
  34. otdf_python/policy_info.py +55 -0
  35. otdf_python/policy_object.py +22 -0
  36. otdf_python/policy_stub.py +2 -0
  37. otdf_python/resource_locator.py +172 -0
  38. otdf_python/sdk.py +436 -0
  39. otdf_python/sdk_builder.py +416 -0
  40. otdf_python/sdk_exceptions.py +16 -0
  41. otdf_python/symmetric_and_payload_config.py +30 -0
  42. otdf_python/tdf.py +480 -0
  43. otdf_python/tdf_reader.py +153 -0
  44. otdf_python/tdf_writer.py +23 -0
  45. otdf_python/token_source.py +34 -0
  46. otdf_python/version.py +57 -0
  47. otdf_python/zip_reader.py +47 -0
  48. otdf_python/zip_writer.py +70 -0
  49. otdf_python-0.3.5.dist-info/METADATA +153 -0
  50. otdf_python-0.3.5.dist-info/RECORD +137 -0
  51. {otdf_python-0.1.10.dist-info → otdf_python-0.3.5.dist-info}/WHEEL +1 -2
  52. {otdf_python-0.1.10.dist-info → otdf_python-0.3.5.dist-info/licenses}/LICENSE +1 -1
  53. otdf_python_proto/__init__.py +37 -0
  54. otdf_python_proto/authorization/__init__.py +1 -0
  55. otdf_python_proto/authorization/authorization_pb2.py +80 -0
  56. otdf_python_proto/authorization/authorization_pb2.pyi +161 -0
  57. otdf_python_proto/authorization/authorization_pb2_connect.py +191 -0
  58. otdf_python_proto/authorization/v2/authorization_pb2.py +105 -0
  59. otdf_python_proto/authorization/v2/authorization_pb2.pyi +134 -0
  60. otdf_python_proto/authorization/v2/authorization_pb2_connect.py +233 -0
  61. otdf_python_proto/common/__init__.py +1 -0
  62. otdf_python_proto/common/common_pb2.py +52 -0
  63. otdf_python_proto/common/common_pb2.pyi +61 -0
  64. otdf_python_proto/entity/__init__.py +1 -0
  65. otdf_python_proto/entity/entity_pb2.py +47 -0
  66. otdf_python_proto/entity/entity_pb2.pyi +50 -0
  67. otdf_python_proto/entityresolution/__init__.py +1 -0
  68. otdf_python_proto/entityresolution/entity_resolution_pb2.py +57 -0
  69. otdf_python_proto/entityresolution/entity_resolution_pb2.pyi +55 -0
  70. otdf_python_proto/entityresolution/entity_resolution_pb2_connect.py +149 -0
  71. otdf_python_proto/entityresolution/v2/entity_resolution_pb2.py +55 -0
  72. otdf_python_proto/entityresolution/v2/entity_resolution_pb2.pyi +55 -0
  73. otdf_python_proto/entityresolution/v2/entity_resolution_pb2_connect.py +149 -0
  74. otdf_python_proto/kas/__init__.py +9 -0
  75. otdf_python_proto/kas/kas_pb2.py +103 -0
  76. otdf_python_proto/kas/kas_pb2.pyi +170 -0
  77. otdf_python_proto/kas/kas_pb2_connect.py +192 -0
  78. otdf_python_proto/legacy_grpc/__init__.py +1 -0
  79. otdf_python_proto/legacy_grpc/authorization/authorization_pb2_grpc.py +163 -0
  80. otdf_python_proto/legacy_grpc/authorization/v2/authorization_pb2_grpc.py +206 -0
  81. otdf_python_proto/legacy_grpc/common/common_pb2_grpc.py +4 -0
  82. otdf_python_proto/legacy_grpc/entity/entity_pb2_grpc.py +4 -0
  83. otdf_python_proto/legacy_grpc/entityresolution/entity_resolution_pb2_grpc.py +122 -0
  84. otdf_python_proto/legacy_grpc/entityresolution/v2/entity_resolution_pb2_grpc.py +120 -0
  85. otdf_python_proto/legacy_grpc/kas/kas_pb2_grpc.py +172 -0
  86. otdf_python_proto/legacy_grpc/logger/audit/test_pb2_grpc.py +4 -0
  87. otdf_python_proto/legacy_grpc/policy/actions/actions_pb2_grpc.py +249 -0
  88. otdf_python_proto/legacy_grpc/policy/attributes/attributes_pb2_grpc.py +873 -0
  89. otdf_python_proto/legacy_grpc/policy/kasregistry/key_access_server_registry_pb2_grpc.py +602 -0
  90. otdf_python_proto/legacy_grpc/policy/keymanagement/key_management_pb2_grpc.py +251 -0
  91. otdf_python_proto/legacy_grpc/policy/namespaces/namespaces_pb2_grpc.py +427 -0
  92. otdf_python_proto/legacy_grpc/policy/objects_pb2_grpc.py +4 -0
  93. otdf_python_proto/legacy_grpc/policy/registeredresources/registered_resources_pb2_grpc.py +524 -0
  94. otdf_python_proto/legacy_grpc/policy/resourcemapping/resource_mapping_pb2_grpc.py +516 -0
  95. otdf_python_proto/legacy_grpc/policy/selectors_pb2_grpc.py +4 -0
  96. otdf_python_proto/legacy_grpc/policy/subjectmapping/subject_mapping_pb2_grpc.py +551 -0
  97. otdf_python_proto/legacy_grpc/policy/unsafe/unsafe_pb2_grpc.py +485 -0
  98. otdf_python_proto/legacy_grpc/wellknownconfiguration/wellknown_configuration_pb2_grpc.py +77 -0
  99. otdf_python_proto/logger/__init__.py +1 -0
  100. otdf_python_proto/logger/audit/test_pb2.py +43 -0
  101. otdf_python_proto/logger/audit/test_pb2.pyi +45 -0
  102. otdf_python_proto/policy/__init__.py +1 -0
  103. otdf_python_proto/policy/actions/actions_pb2.py +75 -0
  104. otdf_python_proto/policy/actions/actions_pb2.pyi +87 -0
  105. otdf_python_proto/policy/actions/actions_pb2_connect.py +275 -0
  106. otdf_python_proto/policy/attributes/attributes_pb2.py +234 -0
  107. otdf_python_proto/policy/attributes/attributes_pb2.pyi +328 -0
  108. otdf_python_proto/policy/attributes/attributes_pb2_connect.py +863 -0
  109. otdf_python_proto/policy/kasregistry/key_access_server_registry_pb2.py +266 -0
  110. otdf_python_proto/policy/kasregistry/key_access_server_registry_pb2.pyi +450 -0
  111. otdf_python_proto/policy/kasregistry/key_access_server_registry_pb2_connect.py +611 -0
  112. otdf_python_proto/policy/keymanagement/key_management_pb2.py +79 -0
  113. otdf_python_proto/policy/keymanagement/key_management_pb2.pyi +87 -0
  114. otdf_python_proto/policy/keymanagement/key_management_pb2_connect.py +275 -0
  115. otdf_python_proto/policy/namespaces/namespaces_pb2.py +117 -0
  116. otdf_python_proto/policy/namespaces/namespaces_pb2.pyi +147 -0
  117. otdf_python_proto/policy/namespaces/namespaces_pb2_connect.py +443 -0
  118. otdf_python_proto/policy/objects_pb2.py +150 -0
  119. otdf_python_proto/policy/objects_pb2.pyi +464 -0
  120. otdf_python_proto/policy/registeredresources/registered_resources_pb2.py +139 -0
  121. otdf_python_proto/policy/registeredresources/registered_resources_pb2.pyi +196 -0
  122. otdf_python_proto/policy/registeredresources/registered_resources_pb2_connect.py +527 -0
  123. otdf_python_proto/policy/resourcemapping/resource_mapping_pb2.py +139 -0
  124. otdf_python_proto/policy/resourcemapping/resource_mapping_pb2.pyi +194 -0
  125. otdf_python_proto/policy/resourcemapping/resource_mapping_pb2_connect.py +527 -0
  126. otdf_python_proto/policy/selectors_pb2.py +57 -0
  127. otdf_python_proto/policy/selectors_pb2.pyi +90 -0
  128. otdf_python_proto/policy/subjectmapping/subject_mapping_pb2.py +127 -0
  129. otdf_python_proto/policy/subjectmapping/subject_mapping_pb2.pyi +189 -0
  130. otdf_python_proto/policy/subjectmapping/subject_mapping_pb2_connect.py +569 -0
  131. otdf_python_proto/policy/unsafe/unsafe_pb2.py +113 -0
  132. otdf_python_proto/policy/unsafe/unsafe_pb2.pyi +145 -0
  133. otdf_python_proto/policy/unsafe/unsafe_pb2_connect.py +485 -0
  134. otdf_python_proto/wellknownconfiguration/__init__.py +1 -0
  135. otdf_python_proto/wellknownconfiguration/wellknown_configuration_pb2.py +51 -0
  136. otdf_python_proto/wellknownconfiguration/wellknown_configuration_pb2.pyi +32 -0
  137. otdf_python_proto/wellknownconfiguration/wellknown_configuration_pb2_connect.py +107 -0
  138. otdf_python/_gotdf_python.cpython-312-darwin.so +0 -0
  139. otdf_python/build.py +0 -190
  140. otdf_python/go.py +0 -1478
  141. otdf_python/gotdf_python.py +0 -383
  142. otdf_python-0.1.10.dist-info/METADATA +0 -149
  143. otdf_python-0.1.10.dist-info/RECORD +0 -10
  144. otdf_python-0.1.10.dist-info/top_level.txt +0 -1
@@ -0,0 +1,132 @@
1
+ """
2
+ NanoTDF ECDSA Signature Structure.
3
+ """
4
+
5
+ from dataclasses import dataclass, field
6
+
7
+
8
+ class IncorrectNanoTDFECDSASignatureSize(Exception):
9
+ """Exception raised when the signature size is incorrect."""
10
+
11
+ pass
12
+
13
+
14
+ @dataclass
15
+ class NanoTDFECDSAStruct:
16
+ """
17
+ Class to handle ECDSA signature structure for NanoTDF.
18
+
19
+ This structure represents an ECDSA signature as required by the NanoTDF format.
20
+ It consists of r and s values along with their lengths.
21
+ """
22
+
23
+ r_length: bytearray = field(default_factory=lambda: bytearray(1))
24
+ r_value: bytearray = None
25
+ s_length: bytearray = field(default_factory=lambda: bytearray(1))
26
+ s_value: bytearray = None
27
+
28
+ @classmethod
29
+ def from_bytes(
30
+ cls, ecdsa_signature_value: bytes, key_size: int
31
+ ) -> "NanoTDFECDSAStruct":
32
+ """
33
+ Create a NanoTDFECDSAStruct from a byte array.
34
+
35
+ Args:
36
+ ecdsa_signature_value: The signature value as bytes
37
+ key_size: The size of the key in bytes
38
+
39
+ Returns:
40
+ A new NanoTDFECDSAStruct
41
+
42
+ Raises:
43
+ IncorrectNanoTDFECDSASignatureSize: If the signature buffer size is invalid
44
+ """
45
+ if len(ecdsa_signature_value) != (2 * key_size) + 2:
46
+ raise IncorrectNanoTDFECDSASignatureSize(
47
+ f"Invalid signature buffer size. Expected {(2 * key_size) + 2}, got {len(ecdsa_signature_value)}"
48
+ )
49
+
50
+ struct_obj = cls()
51
+
52
+ # Copy value of r_length to signature struct
53
+ index = 0
54
+ struct_obj.r_length[0] = ecdsa_signature_value[index]
55
+
56
+ # Copy the contents of r_value to signature struct
57
+ index += 1
58
+ r_len = struct_obj.r_length[0]
59
+ struct_obj.r_value = bytearray(key_size)
60
+ struct_obj.r_value[:r_len] = ecdsa_signature_value[index : index + r_len]
61
+
62
+ # Copy value of s_length to signature struct
63
+ index += key_size
64
+ struct_obj.s_length[0] = ecdsa_signature_value[index]
65
+
66
+ # Copy value of s_value
67
+ index += 1
68
+ s_len = struct_obj.s_length[0]
69
+ struct_obj.s_value = bytearray(key_size)
70
+ struct_obj.s_value[:s_len] = ecdsa_signature_value[index : index + s_len]
71
+
72
+ return struct_obj
73
+
74
+ def as_bytes(self) -> bytes:
75
+ """
76
+ Convert the signature structure to bytes.
77
+ Raises ValueError if r_value or s_value is None.
78
+ """
79
+ if self.r_value is None or self.s_value is None:
80
+ raise ValueError("r_value and s_value must not be None")
81
+ total_size = 1 + len(self.r_value) + 1 + len(self.s_value)
82
+ signature = bytearray(total_size)
83
+
84
+ # Copy value of r_length
85
+ index = 0
86
+ signature[index] = self.r_length[0]
87
+
88
+ # Copy the contents of r_value
89
+ index += 1
90
+ signature[index : index + len(self.r_value)] = self.r_value
91
+
92
+ # Copy value of s_length
93
+ index += len(self.r_value)
94
+ signature[index] = self.s_length[0]
95
+
96
+ # Copy value of s_value
97
+ index += 1
98
+ signature[index : index + len(self.s_value)] = self.s_value
99
+
100
+ return bytes(signature)
101
+
102
+ def get_s_value(self) -> bytearray:
103
+ """Get the s value of the signature."""
104
+ return self.s_value
105
+
106
+ def set_s_value(self, s_value: bytearray) -> None:
107
+ """Set the s value of the signature."""
108
+ self.s_value = s_value
109
+
110
+ def get_s_length(self) -> int:
111
+ """Get the length of the s value."""
112
+ return self.s_length[0]
113
+
114
+ def set_s_length(self, s_length: int) -> None:
115
+ """Set the length of the s value."""
116
+ self.s_length[0] = s_length
117
+
118
+ def get_r_value(self) -> bytearray:
119
+ """Get the r value of the signature."""
120
+ return self.r_value
121
+
122
+ def set_r_value(self, r_value: bytearray) -> None:
123
+ """Set the r value of the signature."""
124
+ self.r_value = r_value
125
+
126
+ def get_r_length(self) -> int:
127
+ """Get the length of the r value."""
128
+ return self.r_length[0]
129
+
130
+ def set_r_length(self, r_length: int) -> None:
131
+ """Set the length of the r value."""
132
+ self.r_length[0] = r_length
@@ -0,0 +1,43 @@
1
+ from enum import Enum
2
+
3
+
4
+ class ECCurve(Enum):
5
+ SECP256R1 = "secp256r1"
6
+ SECP384R1 = "secp384r1"
7
+ SECP521R1 = "secp384r1"
8
+ SECP256K1 = "secp256k1"
9
+
10
+ def __str__(self):
11
+ return self.value
12
+
13
+
14
+ class Protocol(Enum):
15
+ HTTP = "HTTP"
16
+ HTTPS = "HTTPS"
17
+
18
+
19
+ class IdentifierType(Enum):
20
+ NONE = 0
21
+ TWO_BYTES = 2
22
+ EIGHT_BYTES = 8
23
+ THIRTY_TWO_BYTES = 32
24
+
25
+ def get_length(self):
26
+ return self.value
27
+
28
+
29
+ class PolicyType(Enum):
30
+ REMOTE_POLICY = 0
31
+ EMBEDDED_POLICY_PLAIN_TEXT = 1
32
+ EMBEDDED_POLICY_ENCRYPTED = 2
33
+ EMBEDDED_POLICY_ENCRYPTED_POLICY_KEY_ACCESS = 3
34
+
35
+
36
+ class Cipher(Enum):
37
+ AES_256_GCM_64_TAG = 0
38
+ AES_256_GCM_96_TAG = 1
39
+ AES_256_GCM_104_TAG = 2
40
+ AES_256_GCM_112_TAG = 3
41
+ AES_256_GCM_120_TAG = 4
42
+ AES_256_GCM_128_TAG = 5
43
+ EAD_AES_256_HMAC_SHA_256 = 6
@@ -0,0 +1,39 @@
1
+ from typing import Any
2
+
3
+
4
+ class PolicyBinding:
5
+ """
6
+ Represents a policy binding in the TDF manifest.
7
+ This is a placeholder implementation as the complete details of
8
+ the PolicyBinding class aren't provided in the code snippets.
9
+ """
10
+
11
+ def __init__(self, **kwargs):
12
+ for key, value in kwargs.items():
13
+ setattr(self, key, value)
14
+
15
+
16
+ class PolicyBindingSerializer:
17
+ """
18
+ Handles serialization and deserialization of policy bindings.
19
+ This class provides static methods to convert between JSON representations
20
+ and PolicyBinding objects.
21
+ """
22
+
23
+ @staticmethod
24
+ def deserialize(
25
+ json_data: Any, typeofT: type | None = None, context: Any = None
26
+ ) -> Any:
27
+ if isinstance(json_data, dict):
28
+ return PolicyBinding(**json_data)
29
+ if isinstance(json_data, str):
30
+ return json_data
31
+ raise ValueError("Invalid type for PolicyBinding deserialization")
32
+
33
+ @staticmethod
34
+ def serialize(
35
+ src: Any, typeofSrc: type | None = None, context: Any = None
36
+ ) -> dict | str:
37
+ if isinstance(src, PolicyBinding):
38
+ return vars(src)
39
+ return str(src)
@@ -0,0 +1,55 @@
1
+ class PolicyInfo:
2
+ def __init__(
3
+ self,
4
+ policy_type: int = 0,
5
+ body: bytes | None = None,
6
+ ):
7
+ self.policy_type = policy_type
8
+ self.body = body
9
+
10
+ def set_embedded_plain_text_policy(self, body: bytes):
11
+ self.body = body
12
+ self.policy_type = 1 # Placeholder for EMBEDDED_POLICY_PLAIN_TEXT
13
+
14
+ def set_embedded_encrypted_text_policy(self, body: bytes):
15
+ self.body = body
16
+ self.policy_type = 2 # Placeholder for EMBEDDED_POLICY_ENCRYPTED
17
+
18
+ def get_body(self) -> bytes | None:
19
+ return self.body
20
+
21
+ def get_total_size(self) -> int:
22
+ size = 1 # policy_type
23
+ size += 2 # body_len
24
+ size += len(self.body) if self.body else 0
25
+ return size
26
+
27
+ def write_into_buffer(self, buffer: bytearray, offset: int = 0) -> int:
28
+ start = offset
29
+ buffer[offset] = self.policy_type
30
+ offset += 1
31
+ body_len = len(self.body) if self.body else 0
32
+ buffer[offset : offset + 2] = body_len.to_bytes(2, "big")
33
+ offset += 2
34
+ if self.body:
35
+ buffer[offset : offset + body_len] = self.body
36
+ offset += body_len
37
+ return offset - start
38
+
39
+ @staticmethod
40
+ def from_bytes_with_size(buffer: bytes, ecc_mode):
41
+ # Parse policy_type (1 byte), body_len (2 bytes), body
42
+ # Note: binding is NOT part of PolicyInfo - it's read separately in Header
43
+ offset = 0
44
+ if len(buffer) < 3:
45
+ raise ValueError("Buffer too short for PolicyInfo header")
46
+ policy_type = buffer[offset]
47
+ offset += 1
48
+ body_len = int.from_bytes(buffer[offset : offset + 2], "big")
49
+ offset += 2
50
+ if len(buffer) < offset + body_len:
51
+ raise ValueError("Buffer too short for PolicyInfo body")
52
+ body = buffer[offset : offset + body_len]
53
+ offset += body_len
54
+ pi = PolicyInfo(policy_type=policy_type, body=body)
55
+ return pi, offset
@@ -0,0 +1,22 @@
1
+ from dataclasses import dataclass
2
+
3
+
4
+ @dataclass
5
+ class AttributeObject:
6
+ attribute: str
7
+ display_name: str | None = None
8
+ is_default: bool = False
9
+ pub_key: str | None = None
10
+ kas_url: str | None = None
11
+
12
+
13
+ @dataclass
14
+ class PolicyBody:
15
+ data_attributes: list[AttributeObject]
16
+ dissem: list[str]
17
+
18
+
19
+ @dataclass
20
+ class PolicyObject:
21
+ uuid: str
22
+ body: PolicyBody
@@ -0,0 +1,2 @@
1
+ # TODO: Replace this with a proper Policy UUID values
2
+ NULL_POLICY_UUID: str = "00000000-0000-0000-0000-000000000000"
@@ -0,0 +1,172 @@
1
+ class ResourceLocator:
2
+ """
3
+ NanoTDF Resource Locator per the spec:
4
+ https://github.com/opentdf/spec/blob/main/schema/nanotdf/README.md
5
+
6
+ Format:
7
+ - Byte 0: Protocol Enum (bits 0-3) + Identifier Length (bits 4-7)
8
+ - Protocol: 0x0=HTTP, 0x1=HTTPS, 0xF=Shared Resource Directory
9
+ - Identifier: 0x0=None, 0x1=2 bytes, 0x2=8 bytes, 0x3=32 bytes
10
+ - Byte 1: Body Length (1-255 bytes)
11
+ - Bytes 2-N: Body (URL path)
12
+ - Bytes N+1-M: Identifier (optional, 0/2/8/32 bytes)
13
+ """
14
+
15
+ # Protocol enum values
16
+ PROTOCOL_HTTP = 0x0
17
+ PROTOCOL_HTTPS = 0x1
18
+ PROTOCOL_SHARED_RESOURCE_DIR = 0xF
19
+
20
+ # Identifier length enum values (in bits 4-7)
21
+ IDENTIFIER_NONE = 0x0
22
+ IDENTIFIER_2_BYTES = 0x1
23
+ IDENTIFIER_8_BYTES = 0x2
24
+ IDENTIFIER_32_BYTES = 0x3
25
+
26
+ def __init__(self, resource_url: str | None = None, identifier: str | None = None):
27
+ self.resource_url = resource_url or ""
28
+ self.identifier = identifier or ""
29
+
30
+ def get_resource_url(self):
31
+ return self.resource_url
32
+
33
+ def get_identifier(self):
34
+ return self.identifier
35
+
36
+ def _parse_url(self):
37
+ """Parse URL to extract protocol and body (path)."""
38
+ url = self.resource_url
39
+ if url.startswith("https://"):
40
+ protocol = self.PROTOCOL_HTTPS
41
+ body = url[8:] # Remove "https://"
42
+ elif url.startswith("http://"):
43
+ protocol = self.PROTOCOL_HTTP
44
+ body = url[7:] # Remove "http://"
45
+ else:
46
+ # Default to HTTP
47
+ protocol = self.PROTOCOL_HTTP
48
+ body = url
49
+ return protocol, body.encode()
50
+
51
+ def _get_identifier_bytes(self):
52
+ """Get identifier bytes and determine identifier length enum."""
53
+ if not self.identifier:
54
+ return self.IDENTIFIER_NONE, b""
55
+
56
+ id_bytes = self.identifier.encode()
57
+ id_len = len(id_bytes)
58
+
59
+ if id_len == 0:
60
+ return self.IDENTIFIER_NONE, b""
61
+ elif id_len <= 2:
62
+ # Pad to 2 bytes
63
+ return self.IDENTIFIER_2_BYTES, id_bytes.ljust(2, b"\x00")
64
+ elif id_len <= 8:
65
+ # Pad to 8 bytes
66
+ return self.IDENTIFIER_8_BYTES, id_bytes.ljust(8, b"\x00")
67
+ elif id_len <= 32:
68
+ # Pad to 32 bytes
69
+ return self.IDENTIFIER_32_BYTES, id_bytes.ljust(32, b"\x00")
70
+ else:
71
+ raise ValueError(f"Identifier too long: {id_len} bytes (max 32)")
72
+
73
+ def to_bytes(self):
74
+ """
75
+ Convert to NanoTDF Resource Locator format per spec.
76
+
77
+ Format:
78
+ - Byte 0: Protocol Enum (bits 0-3) + Identifier Length (bits 4-7)
79
+ - Byte 1: Body Length
80
+ - Bytes 2-N: Body (URL path)
81
+ - Bytes N+1-M: Identifier (0/2/8/32 bytes)
82
+ """
83
+ protocol, body_bytes = self._parse_url()
84
+ identifier_enum, identifier_bytes = self._get_identifier_bytes()
85
+
86
+ if len(body_bytes) > 255:
87
+ raise ValueError(
88
+ f"Resource Locator body too long: {len(body_bytes)} bytes (max 255)"
89
+ )
90
+
91
+ # Byte 0: protocol in bits 0-3, identifier length in bits 4-7
92
+ protocol_and_id = (identifier_enum << 4) | protocol
93
+
94
+ # Byte 1: body length
95
+ body_len = len(body_bytes)
96
+
97
+ return bytes([protocol_and_id, body_len]) + body_bytes + identifier_bytes
98
+
99
+ def get_total_size(self) -> int:
100
+ return len(self.to_bytes())
101
+
102
+ def write_into_buffer(self, buffer: bytearray, offset: int = 0) -> int:
103
+ data = self.to_bytes()
104
+ buffer[offset : offset + len(data)] = data
105
+ return len(data)
106
+
107
+ @staticmethod
108
+ def from_bytes_with_size(buffer: bytes): # noqa: C901
109
+ """
110
+ Parse NanoTDF Resource Locator from bytes per spec.
111
+
112
+ Format:
113
+ - Byte 0: Protocol Enum (bits 0-3) + Identifier Length (bits 4-7)
114
+ - Byte 1: Body Length
115
+ - Bytes 2-N: Body (URL path)
116
+ - Bytes N+1-M: Identifier (0/2/8/32 bytes)
117
+ """
118
+ if len(buffer) < 2:
119
+ raise ValueError("Buffer too short for ResourceLocator")
120
+
121
+ # Parse byte 0: protocol and identifier length
122
+ protocol_and_id = buffer[0]
123
+ protocol = protocol_and_id & 0x0F # Bits 0-3
124
+ identifier_enum = (protocol_and_id >> 4) & 0x0F # Bits 4-7
125
+
126
+ # Parse byte 1: body length
127
+ body_len = buffer[1]
128
+
129
+ if len(buffer) < 2 + body_len:
130
+ raise ValueError(
131
+ f"Buffer too short for ResourceLocator body (need {2 + body_len}, have {len(buffer)})"
132
+ )
133
+
134
+ # Parse body (URL path)
135
+ body_bytes = buffer[2 : 2 + body_len]
136
+ body = body_bytes.decode()
137
+
138
+ # Reconstruct full URL with protocol
139
+ if protocol == ResourceLocator.PROTOCOL_HTTPS:
140
+ resource_url = f"https://{body}"
141
+ elif protocol == ResourceLocator.PROTOCOL_HTTP:
142
+ resource_url = f"http://{body}"
143
+ else:
144
+ resource_url = body
145
+
146
+ # Parse identifier based on identifier_enum
147
+ offset = 2 + body_len
148
+ if identifier_enum == ResourceLocator.IDENTIFIER_NONE:
149
+ identifier_len = 0
150
+ elif identifier_enum == ResourceLocator.IDENTIFIER_2_BYTES:
151
+ identifier_len = 2
152
+ elif identifier_enum == ResourceLocator.IDENTIFIER_8_BYTES:
153
+ identifier_len = 8
154
+ elif identifier_enum == ResourceLocator.IDENTIFIER_32_BYTES:
155
+ identifier_len = 32
156
+ else:
157
+ raise ValueError(f"Invalid identifier length enum: {identifier_enum}")
158
+
159
+ if len(buffer) < offset + identifier_len:
160
+ raise ValueError(
161
+ f"Buffer too short for ResourceLocator identifier (need {offset + identifier_len}, have {len(buffer)})"
162
+ )
163
+
164
+ if identifier_len > 0:
165
+ identifier_bytes = buffer[offset : offset + identifier_len]
166
+ # Remove padding
167
+ identifier = identifier_bytes.rstrip(b"\x00").decode()
168
+ else:
169
+ identifier = ""
170
+
171
+ size = 2 + body_len + identifier_len
172
+ return ResourceLocator(resource_url, identifier), size