onec-interactive-runtime-core 0.1.19__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.
- onec_interactive_runtime_core-0.1.19.dist-info/METADATA +12 -0
- onec_interactive_runtime_core-0.1.19.dist-info/RECORD +87 -0
- onec_interactive_runtime_core-0.1.19.dist-info/WHEEL +4 -0
- onec_interactive_runtime_core-0.1.19.dist-info/licenses/COPYRIGHT +9 -0
- onec_interactive_runtime_core-0.1.19.dist-info/licenses/LICENSE +232 -0
- onec_interactive_runtime_core-0.1.19.dist-info/licenses/THIRD_PARTY_NOTICES.txt +32 -0
- onec_runtime/AGENTS.md +7 -0
- onec_runtime/__init__.py +3 -0
- onec_runtime/artifacts.py +59 -0
- onec_runtime/bootstrap.py +310 -0
- onec_runtime/breakpoint_workspace.py +249 -0
- onec_runtime/bsl/__init__.py +139 -0
- onec_runtime/bsl/diagnostics.py +1046 -0
- onec_runtime/bsl/full_ast_worker_projection.py +522 -0
- onec_runtime/bsl/generated_semantic_parser.py +6322 -0
- onec_runtime/bsl/lexer.py +164 -0
- onec_runtime/bsl/module_catalog.py +420 -0
- onec_runtime/bsl/module_delta.py +1079 -0
- onec_runtime/bsl/module_universe.py +1877 -0
- onec_runtime/bsl/notebook_cells.py +232 -0
- onec_runtime/bsl/notebook_method_globals.py +74 -0
- onec_runtime/bsl/notebook_methods.py +175 -0
- onec_runtime/bsl/parser_artifact_identity.py +119 -0
- onec_runtime/bsl/parser_target.py +388 -0
- onec_runtime/bsl/preprocessor.py +16 -0
- onec_runtime/bsl/semantic_lowering.py +1539 -0
- onec_runtime/bsl/source_maps.py +2411 -0
- onec_runtime/bsl/worker_dependency_resolver.py +289 -0
- onec_runtime/bsl/worker_preprocessor.py +247 -0
- onec_runtime/bsl/worker_projection_model.py +137 -0
- onec_runtime/bsl/worker_reload_source_map.py +1697 -0
- onec_runtime/capture.py +113 -0
- onec_runtime/capture_source.py +453 -0
- onec_runtime/compact_table.py +376 -0
- onec_runtime/compact_table_backend.py +500 -0
- onec_runtime/config.py +198 -0
- onec_runtime/configurator_agent.py +520 -0
- onec_runtime/controller_worker.py +400 -0
- onec_runtime/credentials.py +18 -0
- onec_runtime/epf_container.py +196 -0
- onec_runtime/errors.py +205 -0
- onec_runtime/experiment.py +130 -0
- onec_runtime/extension_bundle.py +944 -0
- onec_runtime/extension_lifecycle.py +402 -0
- onec_runtime/extension_state.py +222 -0
- onec_runtime/fault_injection.py +30 -0
- onec_runtime/kernel.py +179 -0
- onec_runtime/lease.py +123 -0
- onec_runtime/observation.py +287 -0
- onec_runtime/performance_profile.py +245 -0
- onec_runtime/privacy.py +263 -0
- onec_runtime/processes.py +265 -0
- onec_runtime/prototype_runtime.py +2644 -0
- onec_runtime/rdbg/__init__.py +1 -0
- onec_runtime/rdbg/models.py +130 -0
- onec_runtime/rdbg/reconnect.py +59 -0
- onec_runtime/rdbg/session.py +913 -0
- onec_runtime/rdbg/transport.py +138 -0
- onec_runtime/rdbg/xml_codec.py +980 -0
- onec_runtime/recovery.py +106 -0
- onec_runtime/recovery_journal.py +79 -0
- onec_runtime/resources/extension/OnecInteractiveRuntime.cfe +0 -0
- onec_runtime/resources/extension/extension-manifest.json +108 -0
- onec_runtime/runtime_api.py +5272 -0
- onec_runtime/runtime_contracts.py +196 -0
- onec_runtime/server_worker.py +2034 -0
- onec_runtime/session.py +2123 -0
- onec_runtime/startup_diagnostics.py +45 -0
- onec_runtime/stop_routing.py +65 -0
- onec_runtime/supervised_processes.py +232 -0
- onec_runtime/supervised_runtime.py +512 -0
- onec_runtime/supervisor.py +363 -0
- onec_runtime/supervisor_model.py +148 -0
- onec_runtime/supervisor_protocol.py +77 -0
- onec_runtime/table_materialization.py +471 -0
- onec_runtime/table_transfer_backend.py +203 -0
- onec_runtime/table_value.py +311 -0
- onec_runtime/toolchain.py +730 -0
- onec_runtime/value_materialization.py +432 -0
- onec_runtime/value_transfer_backend.py +199 -0
- onec_runtime/worker_breakpoints.py +1431 -0
- onec_runtime/worker_epf.py +313 -0
- onec_runtime/worker_epf_template.py +53 -0
- onec_runtime/worker_stage_protocol.py +516 -0
- onec_runtime/worker_universe.py +3772 -0
- onec_runtime_build/__init__.py +1 -0
- onec_runtime_build/parser_target_development.py +116 -0
|
@@ -0,0 +1,944 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib.resources
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import shutil
|
|
8
|
+
import tempfile
|
|
9
|
+
from collections.abc import Mapping
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from hashlib import sha256
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from uuid import UUID
|
|
14
|
+
from xml.etree import ElementTree
|
|
15
|
+
|
|
16
|
+
from onec_runtime.errors import ExtensionBundleError
|
|
17
|
+
from onec_runtime.rdbg.models import ModuleLocation
|
|
18
|
+
|
|
19
|
+
EXTENSION_NAME = "OnecInteractiveRuntime"
|
|
20
|
+
PRODUCT_ID = "onec-interactive-runtime"
|
|
21
|
+
|
|
22
|
+
_MANIFEST_FILENAME = "extension-manifest.json"
|
|
23
|
+
_MANIFEST_SCHEMA_VERSION = 1
|
|
24
|
+
_MD_NAMESPACE = "http://v8.1c.ru/8.3/MDClasses"
|
|
25
|
+
_DUMP_NAMESPACE = "http://v8.1c.ru/8.3/xcf/dumpinfo"
|
|
26
|
+
_MANAGED_SOURCE = "Ext/ManagedApplicationModule.bsl"
|
|
27
|
+
_SERVER_SOURCE = "CommonModules/RuntimeKernelServer/Ext/Module.bsl"
|
|
28
|
+
_PERMANENT_IDENTITY_RUNTIME_MODULE_NAMES = (
|
|
29
|
+
"RuntimeContextStoreServer",
|
|
30
|
+
"RuntimeKernelServer",
|
|
31
|
+
"RuntimeTableTransferServer",
|
|
32
|
+
"RuntimeValueTransferServer",
|
|
33
|
+
)
|
|
34
|
+
_MANAGED_APPLICATION_MODULE_PROPERTY_ID = UUID("d22e852a-cf8a-4f77-8ccb-3548e7792bea")
|
|
35
|
+
_COMMON_MODULE_PROPERTY_ID = UUID("d5963243-262e-4398-b4d7-fb16d06484f6")
|
|
36
|
+
_SHA256 = re.compile(r"[0-9a-f]{64}\Z")
|
|
37
|
+
_HANDSHAKE_ASSIGNMENTS = {
|
|
38
|
+
"product_id": re.compile(r'^\s*ИдентификаторПродуктаRuntime\s*=\s*"([^"]+)";\s*$'),
|
|
39
|
+
"artifact_version": re.compile(r'^\s*ВерсияАртефактаRuntime\s*=\s*"([^"]+)";\s*$'),
|
|
40
|
+
"protocol_version": re.compile(r'^\s*ВерсияПротоколаRuntime\s*=\s*"([^"]+)";\s*$'),
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True, slots=True)
|
|
45
|
+
class MetadataIdentity:
|
|
46
|
+
name: str
|
|
47
|
+
object_id: UUID
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass(frozen=True, slots=True)
|
|
51
|
+
class PermanentExtensionIdentity:
|
|
52
|
+
product_id: str
|
|
53
|
+
extension_name: str
|
|
54
|
+
root_id: UUID
|
|
55
|
+
runtime_module_ids: tuple[UUID, ...]
|
|
56
|
+
purpose: str
|
|
57
|
+
name_prefix: str
|
|
58
|
+
vendor: str
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass(frozen=True, slots=True)
|
|
62
|
+
class ExactExtensionArtifact:
|
|
63
|
+
artifact_version: str
|
|
64
|
+
protocol_version: str
|
|
65
|
+
language_bound_by_name: bool
|
|
66
|
+
metadata: tuple[MetadataIdentity, ...]
|
|
67
|
+
source_sha256: tuple[tuple[str, str], ...]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass(frozen=True, slots=True)
|
|
71
|
+
class ExtensionFingerprints:
|
|
72
|
+
identity: PermanentExtensionIdentity
|
|
73
|
+
artifact: ExactExtensionArtifact
|
|
74
|
+
identity_sha256: str
|
|
75
|
+
artifact_sha256: str
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass(frozen=True, slots=True)
|
|
79
|
+
class BreakpointContract:
|
|
80
|
+
managed: ModuleLocation
|
|
81
|
+
server_entry: ModuleLocation
|
|
82
|
+
server_service: ModuleLocation
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass(frozen=True, slots=True)
|
|
86
|
+
class ExtensionHandshakeEvidence:
|
|
87
|
+
target_type: str
|
|
88
|
+
product_id: str
|
|
89
|
+
artifact_version: str
|
|
90
|
+
protocol_version: str
|
|
91
|
+
location: ModuleLocation
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclass(frozen=True, slots=True)
|
|
95
|
+
class ExtensionManifest:
|
|
96
|
+
schema_version: int
|
|
97
|
+
product_id: str
|
|
98
|
+
extension_name: str
|
|
99
|
+
artifact_version: str
|
|
100
|
+
protocol_version: str
|
|
101
|
+
cfe_filename: str
|
|
102
|
+
cfe_size: int
|
|
103
|
+
cfe_sha256: str
|
|
104
|
+
fingerprints: ExtensionFingerprints
|
|
105
|
+
breakpoints: BreakpointContract
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@dataclass(frozen=True, slots=True)
|
|
109
|
+
class ExtensionBundle:
|
|
110
|
+
cfe_path: Path
|
|
111
|
+
manifest: ExtensionManifest
|
|
112
|
+
cache_dir: Path
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _fail(message: str) -> ExtensionBundleError:
|
|
116
|
+
return ExtensionBundleError(message)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _object(value: object, *, label: str) -> dict[str, object]:
|
|
120
|
+
if not isinstance(value, dict) or any(not isinstance(key, str) for key in value):
|
|
121
|
+
raise _fail(f"{label} must be a JSON object")
|
|
122
|
+
return value
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _exact_keys(value: Mapping[str, object], expected: set[str], *, label: str) -> None:
|
|
126
|
+
unknown = sorted(set(value) - expected)
|
|
127
|
+
missing = sorted(expected - set(value))
|
|
128
|
+
if unknown:
|
|
129
|
+
raise _fail(f"{label} contains unknown properties: {', '.join(unknown)}")
|
|
130
|
+
if missing:
|
|
131
|
+
raise _fail(f"{label} is missing required properties: {', '.join(missing)}")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _string(value: object, *, label: str) -> str:
|
|
135
|
+
if not isinstance(value, str) or not value:
|
|
136
|
+
raise _fail(f"{label} must be a non-empty string")
|
|
137
|
+
return value
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _integer(value: object, *, label: str, minimum: int = 0) -> int:
|
|
141
|
+
if isinstance(value, bool) or not isinstance(value, int) or value < minimum:
|
|
142
|
+
raise _fail(f"{label} must be an integer greater than or equal to {minimum}")
|
|
143
|
+
return value
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _boolean(value: object, *, label: str) -> bool:
|
|
147
|
+
if not isinstance(value, bool):
|
|
148
|
+
raise _fail(f"{label} must be a boolean")
|
|
149
|
+
return value
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _uuid(value: object, *, label: str) -> UUID:
|
|
153
|
+
text = _string(value, label=label)
|
|
154
|
+
try:
|
|
155
|
+
parsed = UUID(text)
|
|
156
|
+
except ValueError as error:
|
|
157
|
+
raise _fail(f"{label} must be a valid UUID") from error
|
|
158
|
+
if str(parsed) != text:
|
|
159
|
+
raise _fail(f"{label} must be a canonical lowercase UUID")
|
|
160
|
+
return parsed
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _hash(value: object, *, label: str) -> str:
|
|
164
|
+
text = _string(value, label=label)
|
|
165
|
+
if _SHA256.fullmatch(text) is None:
|
|
166
|
+
raise _fail(f"{label} must be a lowercase SHA-256 digest")
|
|
167
|
+
return text
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _canonical_sha256(value: object) -> str:
|
|
171
|
+
encoded = json.dumps(
|
|
172
|
+
value,
|
|
173
|
+
ensure_ascii=False,
|
|
174
|
+
sort_keys=True,
|
|
175
|
+
separators=(",", ":"),
|
|
176
|
+
).encode("utf-8")
|
|
177
|
+
return sha256(encoded).hexdigest()
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _identity_json(identity: PermanentExtensionIdentity) -> dict[str, object]:
|
|
181
|
+
return {
|
|
182
|
+
"product_id": identity.product_id,
|
|
183
|
+
"extension_name": identity.extension_name,
|
|
184
|
+
"root_id": str(identity.root_id),
|
|
185
|
+
"runtime_module_ids": [str(value) for value in identity.runtime_module_ids],
|
|
186
|
+
"purpose": identity.purpose,
|
|
187
|
+
"name_prefix": identity.name_prefix,
|
|
188
|
+
"vendor": identity.vendor,
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _artifact_json(artifact: ExactExtensionArtifact) -> dict[str, object]:
|
|
193
|
+
return {
|
|
194
|
+
"artifact_version": artifact.artifact_version,
|
|
195
|
+
"protocol_version": artifact.protocol_version,
|
|
196
|
+
"language_bound_by_name": artifact.language_bound_by_name,
|
|
197
|
+
"metadata": [
|
|
198
|
+
{"name": item.name, "object_id": str(item.object_id)}
|
|
199
|
+
for item in artifact.metadata
|
|
200
|
+
],
|
|
201
|
+
"source_sha256": dict(artifact.source_sha256),
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _parse_identity(value: object) -> PermanentExtensionIdentity:
|
|
206
|
+
payload = _object(value, label="fingerprints.identity")
|
|
207
|
+
_exact_keys(
|
|
208
|
+
payload,
|
|
209
|
+
{
|
|
210
|
+
"product_id",
|
|
211
|
+
"extension_name",
|
|
212
|
+
"root_id",
|
|
213
|
+
"runtime_module_ids",
|
|
214
|
+
"purpose",
|
|
215
|
+
"name_prefix",
|
|
216
|
+
"vendor",
|
|
217
|
+
},
|
|
218
|
+
label="fingerprints.identity",
|
|
219
|
+
)
|
|
220
|
+
module_values = payload["runtime_module_ids"]
|
|
221
|
+
if not isinstance(module_values, list) or not module_values:
|
|
222
|
+
raise _fail(
|
|
223
|
+
"fingerprints.identity.runtime_module_ids must be a non-empty array"
|
|
224
|
+
)
|
|
225
|
+
module_ids = tuple(
|
|
226
|
+
_uuid(item, label=f"fingerprints.identity.runtime_module_ids[{index}]")
|
|
227
|
+
for index, item in enumerate(module_values)
|
|
228
|
+
)
|
|
229
|
+
if len(set(module_ids)) != len(module_ids):
|
|
230
|
+
raise _fail("fingerprints.identity.runtime_module_ids contains duplicate UUIDs")
|
|
231
|
+
identity = PermanentExtensionIdentity(
|
|
232
|
+
product_id=_string(
|
|
233
|
+
payload["product_id"], label="fingerprints.identity.product_id"
|
|
234
|
+
),
|
|
235
|
+
extension_name=_string(
|
|
236
|
+
payload["extension_name"], label="fingerprints.identity.extension_name"
|
|
237
|
+
),
|
|
238
|
+
root_id=_uuid(payload["root_id"], label="fingerprints.identity.root_id"),
|
|
239
|
+
runtime_module_ids=module_ids,
|
|
240
|
+
purpose=_string(payload["purpose"], label="fingerprints.identity.purpose"),
|
|
241
|
+
name_prefix=_string(
|
|
242
|
+
payload["name_prefix"], label="fingerprints.identity.name_prefix"
|
|
243
|
+
),
|
|
244
|
+
vendor=_string(payload["vendor"], label="fingerprints.identity.vendor"),
|
|
245
|
+
)
|
|
246
|
+
if identity.product_id != PRODUCT_ID:
|
|
247
|
+
raise _fail("fingerprints.identity.product_id is not this product")
|
|
248
|
+
if identity.extension_name != EXTENSION_NAME:
|
|
249
|
+
raise _fail(
|
|
250
|
+
"fingerprints.identity.extension_name is not the packaged extension"
|
|
251
|
+
)
|
|
252
|
+
if identity.purpose != "AddOn":
|
|
253
|
+
raise _fail("fingerprints.identity.purpose must be AddOn")
|
|
254
|
+
if identity.name_prefix != f"{EXTENSION_NAME}_":
|
|
255
|
+
raise _fail("fingerprints.identity.name_prefix is invalid")
|
|
256
|
+
if identity.vendor != PRODUCT_ID:
|
|
257
|
+
raise _fail("fingerprints.identity.vendor is invalid")
|
|
258
|
+
return identity
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _safe_relative_source(value: object, *, label: str) -> str:
|
|
262
|
+
text = _string(value, label=label)
|
|
263
|
+
path = Path(text)
|
|
264
|
+
if (
|
|
265
|
+
path.is_absolute()
|
|
266
|
+
or path.as_posix() != text
|
|
267
|
+
or not path.parts
|
|
268
|
+
or any(part in {"", ".", ".."} for part in path.parts)
|
|
269
|
+
or path.suffix.casefold() != ".bsl"
|
|
270
|
+
):
|
|
271
|
+
raise _fail(f"{label} must be a normalized relative BSL path")
|
|
272
|
+
return text
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _parse_artifact(value: object) -> ExactExtensionArtifact:
|
|
276
|
+
payload = _object(value, label="fingerprints.artifact")
|
|
277
|
+
_exact_keys(
|
|
278
|
+
payload,
|
|
279
|
+
{
|
|
280
|
+
"artifact_version",
|
|
281
|
+
"protocol_version",
|
|
282
|
+
"language_bound_by_name",
|
|
283
|
+
"metadata",
|
|
284
|
+
"source_sha256",
|
|
285
|
+
},
|
|
286
|
+
label="fingerprints.artifact",
|
|
287
|
+
)
|
|
288
|
+
metadata_values = payload["metadata"]
|
|
289
|
+
if not isinstance(metadata_values, list) or not metadata_values:
|
|
290
|
+
raise _fail("fingerprints.artifact.metadata must be a non-empty array")
|
|
291
|
+
metadata: list[MetadataIdentity] = []
|
|
292
|
+
names: set[str] = set()
|
|
293
|
+
for index, raw in enumerate(metadata_values):
|
|
294
|
+
item = _object(raw, label=f"fingerprints.artifact.metadata[{index}]")
|
|
295
|
+
_exact_keys(
|
|
296
|
+
item,
|
|
297
|
+
{"name", "object_id"},
|
|
298
|
+
label=f"fingerprints.artifact.metadata[{index}]",
|
|
299
|
+
)
|
|
300
|
+
name = _string(
|
|
301
|
+
item["name"], label=f"fingerprints.artifact.metadata[{index}].name"
|
|
302
|
+
)
|
|
303
|
+
if name in names:
|
|
304
|
+
raise _fail(
|
|
305
|
+
f"fingerprints.artifact.metadata has duplicate metadata name {name!r}"
|
|
306
|
+
)
|
|
307
|
+
names.add(name)
|
|
308
|
+
metadata.append(
|
|
309
|
+
MetadataIdentity(
|
|
310
|
+
name,
|
|
311
|
+
_uuid(
|
|
312
|
+
item["object_id"],
|
|
313
|
+
label=f"fingerprints.artifact.metadata[{index}].object_id",
|
|
314
|
+
),
|
|
315
|
+
)
|
|
316
|
+
)
|
|
317
|
+
sources = _object(
|
|
318
|
+
payload["source_sha256"], label="fingerprints.artifact.source_sha256"
|
|
319
|
+
)
|
|
320
|
+
source_hashes = tuple(
|
|
321
|
+
sorted(
|
|
322
|
+
(
|
|
323
|
+
_safe_relative_source(
|
|
324
|
+
name, label="fingerprints.artifact.source_sha256 key"
|
|
325
|
+
),
|
|
326
|
+
_hash(digest, label=f"fingerprints.artifact.source_sha256[{name!r}]"),
|
|
327
|
+
)
|
|
328
|
+
for name, digest in sources.items()
|
|
329
|
+
)
|
|
330
|
+
)
|
|
331
|
+
if {name for name, _ in source_hashes} != {_MANAGED_SOURCE, _SERVER_SOURCE}:
|
|
332
|
+
raise _fail(
|
|
333
|
+
"fingerprints.artifact.source_sha256 must bind both runtime BSL modules"
|
|
334
|
+
)
|
|
335
|
+
return ExactExtensionArtifact(
|
|
336
|
+
artifact_version=_string(
|
|
337
|
+
payload["artifact_version"], label="fingerprints.artifact.artifact_version"
|
|
338
|
+
),
|
|
339
|
+
protocol_version=_string(
|
|
340
|
+
payload["protocol_version"], label="fingerprints.artifact.protocol_version"
|
|
341
|
+
),
|
|
342
|
+
language_bound_by_name=_boolean(
|
|
343
|
+
payload["language_bound_by_name"],
|
|
344
|
+
label="fingerprints.artifact.language_bound_by_name",
|
|
345
|
+
),
|
|
346
|
+
metadata=tuple(metadata),
|
|
347
|
+
source_sha256=source_hashes,
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def _parse_fingerprints(value: object) -> ExtensionFingerprints:
|
|
352
|
+
payload = _object(value, label="fingerprints")
|
|
353
|
+
_exact_keys(
|
|
354
|
+
payload,
|
|
355
|
+
{"identity", "artifact", "identity_sha256", "artifact_sha256"},
|
|
356
|
+
label="fingerprints",
|
|
357
|
+
)
|
|
358
|
+
identity = _parse_identity(payload["identity"])
|
|
359
|
+
artifact = _parse_artifact(payload["artifact"])
|
|
360
|
+
identity_hash = _hash(
|
|
361
|
+
payload["identity_sha256"], label="fingerprints.identity_sha256"
|
|
362
|
+
)
|
|
363
|
+
artifact_hash = _hash(
|
|
364
|
+
payload["artifact_sha256"], label="fingerprints.artifact_sha256"
|
|
365
|
+
)
|
|
366
|
+
if identity_hash != _canonical_sha256(_identity_json(identity)):
|
|
367
|
+
raise _fail("identity fingerprint SHA-256 does not match its contract")
|
|
368
|
+
if artifact_hash != _canonical_sha256(_artifact_json(artifact)):
|
|
369
|
+
raise _fail(
|
|
370
|
+
"artifact fingerprint SHA-256 does not match its source hashes and contract"
|
|
371
|
+
)
|
|
372
|
+
return ExtensionFingerprints(
|
|
373
|
+
identity=identity,
|
|
374
|
+
artifact=artifact,
|
|
375
|
+
identity_sha256=identity_hash,
|
|
376
|
+
artifact_sha256=artifact_hash,
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _parse_location(value: object, *, label: str) -> ModuleLocation:
|
|
381
|
+
payload = _object(value, label=label)
|
|
382
|
+
_exact_keys(
|
|
383
|
+
payload,
|
|
384
|
+
{
|
|
385
|
+
"module_type",
|
|
386
|
+
"url",
|
|
387
|
+
"object_id",
|
|
388
|
+
"property_id",
|
|
389
|
+
"line",
|
|
390
|
+
"extension_name",
|
|
391
|
+
"ext_id",
|
|
392
|
+
},
|
|
393
|
+
label=label,
|
|
394
|
+
)
|
|
395
|
+
url = payload["url"]
|
|
396
|
+
if not isinstance(url, str):
|
|
397
|
+
raise _fail(f"{label}.url must be a string")
|
|
398
|
+
location = ModuleLocation(
|
|
399
|
+
module_type=_string(payload["module_type"], label=f"{label}.module_type"),
|
|
400
|
+
url=url,
|
|
401
|
+
object_id=_uuid(payload["object_id"], label=f"{label}.object_id"),
|
|
402
|
+
property_id=_uuid(payload["property_id"], label=f"{label}.property_id"),
|
|
403
|
+
line=_integer(payload["line"], label=f"{label}.line", minimum=1),
|
|
404
|
+
extension_name=_string(
|
|
405
|
+
payload["extension_name"], label=f"{label}.extension_name"
|
|
406
|
+
),
|
|
407
|
+
ext_id=_integer(payload["ext_id"], label=f"{label}.ext_id"),
|
|
408
|
+
)
|
|
409
|
+
if location.extension_name != EXTENSION_NAME:
|
|
410
|
+
raise _fail(f"{label}.extension_name is not the packaged extension")
|
|
411
|
+
return location
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def _parse_breakpoints(
|
|
415
|
+
value: object, fingerprints: ExtensionFingerprints
|
|
416
|
+
) -> BreakpointContract:
|
|
417
|
+
payload = _object(value, label="breakpoints")
|
|
418
|
+
_exact_keys(
|
|
419
|
+
payload,
|
|
420
|
+
{"managed", "server_entry", "server_service"},
|
|
421
|
+
label="breakpoints",
|
|
422
|
+
)
|
|
423
|
+
result = BreakpointContract(
|
|
424
|
+
managed=_parse_location(payload["managed"], label="breakpoints.managed"),
|
|
425
|
+
server_entry=_parse_location(
|
|
426
|
+
payload["server_entry"], label="breakpoints.server_entry"
|
|
427
|
+
),
|
|
428
|
+
server_service=_parse_location(
|
|
429
|
+
payload["server_service"], label="breakpoints.server_service"
|
|
430
|
+
),
|
|
431
|
+
)
|
|
432
|
+
if result.managed.module_type != "ExtensionModule":
|
|
433
|
+
raise _fail("breakpoints.managed.module_type must be ExtensionModule")
|
|
434
|
+
if result.managed.object_id != fingerprints.identity.root_id:
|
|
435
|
+
raise _fail("breakpoints.managed.object_id is invalid")
|
|
436
|
+
if result.managed.property_id != _MANAGED_APPLICATION_MODULE_PROPERTY_ID:
|
|
437
|
+
raise _fail("breakpoints.managed.property_id is invalid")
|
|
438
|
+
server_object_ids = {
|
|
439
|
+
item.object_id
|
|
440
|
+
for item in fingerprints.artifact.metadata
|
|
441
|
+
if item.name == "CommonModule.RuntimeKernelServer"
|
|
442
|
+
}
|
|
443
|
+
if len(server_object_ids) != 1:
|
|
444
|
+
raise _fail(
|
|
445
|
+
"fingerprints.artifact.metadata must identify CommonModule.RuntimeKernelServer"
|
|
446
|
+
)
|
|
447
|
+
server_object_id = next(iter(server_object_ids))
|
|
448
|
+
for label, location in (
|
|
449
|
+
("server_entry", result.server_entry),
|
|
450
|
+
("server_service", result.server_service),
|
|
451
|
+
):
|
|
452
|
+
if location.module_type != "ExtensionModule":
|
|
453
|
+
raise _fail(f"breakpoints.{label}.module_type must be ExtensionModule")
|
|
454
|
+
if location.property_id != _COMMON_MODULE_PROPERTY_ID:
|
|
455
|
+
raise _fail(f"breakpoints.{label}.property_id is invalid")
|
|
456
|
+
if location.object_id != server_object_id:
|
|
457
|
+
raise _fail(
|
|
458
|
+
f"breakpoints.{label}.object_id disagrees with RuntimeKernelServer metadata"
|
|
459
|
+
)
|
|
460
|
+
if location.url or location.ext_id != 0:
|
|
461
|
+
raise _fail(f"breakpoints.{label} has invalid URL or extension id")
|
|
462
|
+
if result.managed.url or result.managed.ext_id != 0:
|
|
463
|
+
raise _fail("breakpoints.managed has invalid URL or extension id")
|
|
464
|
+
if (
|
|
465
|
+
result.server_entry.object_id != result.server_service.object_id
|
|
466
|
+
or result.server_entry.property_id != result.server_service.property_id
|
|
467
|
+
or result.server_entry.url != result.server_service.url
|
|
468
|
+
or result.server_entry.ext_id != result.server_service.ext_id
|
|
469
|
+
or result.server_entry.line == result.server_service.line
|
|
470
|
+
):
|
|
471
|
+
raise _fail("server breakpoint coordinates do not describe one module")
|
|
472
|
+
return result
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def _unique_json_object(pairs: list[tuple[str, object]]) -> dict[str, object]:
|
|
476
|
+
result: dict[str, object] = {}
|
|
477
|
+
for key, value in pairs:
|
|
478
|
+
if key in result:
|
|
479
|
+
raise _fail(f"extension manifest contains duplicate JSON property {key!r}")
|
|
480
|
+
result[key] = value
|
|
481
|
+
return result
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def read_extension_manifest(path: Path) -> ExtensionManifest:
|
|
485
|
+
try:
|
|
486
|
+
payload = json.loads(
|
|
487
|
+
path.read_text(encoding="utf-8"), object_pairs_hook=_unique_json_object
|
|
488
|
+
)
|
|
489
|
+
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as error:
|
|
490
|
+
raise _fail(
|
|
491
|
+
f"extension manifest is unreadable or invalid JSON: {path}"
|
|
492
|
+
) from error
|
|
493
|
+
root = _object(payload, label="extension manifest")
|
|
494
|
+
_exact_keys(
|
|
495
|
+
root,
|
|
496
|
+
{
|
|
497
|
+
"schema_version",
|
|
498
|
+
"product_id",
|
|
499
|
+
"extension_name",
|
|
500
|
+
"artifact_version",
|
|
501
|
+
"protocol_version",
|
|
502
|
+
"cfe_filename",
|
|
503
|
+
"cfe_size",
|
|
504
|
+
"cfe_sha256",
|
|
505
|
+
"fingerprints",
|
|
506
|
+
"breakpoints",
|
|
507
|
+
},
|
|
508
|
+
label="extension manifest",
|
|
509
|
+
)
|
|
510
|
+
schema_version = _integer(root["schema_version"], label="schema_version", minimum=1)
|
|
511
|
+
if schema_version != _MANIFEST_SCHEMA_VERSION:
|
|
512
|
+
raise _fail(f"unsupported extension manifest schema version: {schema_version}")
|
|
513
|
+
product_id = _string(root["product_id"], label="product_id")
|
|
514
|
+
extension_name = _string(root["extension_name"], label="extension_name")
|
|
515
|
+
artifact_version = _string(root["artifact_version"], label="artifact_version")
|
|
516
|
+
protocol_version = _string(root["protocol_version"], label="protocol_version")
|
|
517
|
+
cfe_filename = _string(root["cfe_filename"], label="cfe_filename")
|
|
518
|
+
if product_id != PRODUCT_ID:
|
|
519
|
+
raise _fail("manifest product_id is not this product")
|
|
520
|
+
if extension_name != EXTENSION_NAME:
|
|
521
|
+
raise _fail("manifest extension_name is not the packaged extension")
|
|
522
|
+
if cfe_filename != f"{EXTENSION_NAME}.cfe":
|
|
523
|
+
raise _fail("manifest cfe_filename is invalid")
|
|
524
|
+
fingerprints = _parse_fingerprints(root["fingerprints"])
|
|
525
|
+
if (
|
|
526
|
+
fingerprints.identity.product_id != product_id
|
|
527
|
+
or fingerprints.identity.extension_name != extension_name
|
|
528
|
+
):
|
|
529
|
+
raise _fail(
|
|
530
|
+
"manifest identity fingerprint disagrees with the manifest identity"
|
|
531
|
+
)
|
|
532
|
+
root_metadata_ids = {
|
|
533
|
+
item.object_id
|
|
534
|
+
for item in fingerprints.artifact.metadata
|
|
535
|
+
if item.name == f"Configuration.{extension_name}"
|
|
536
|
+
}
|
|
537
|
+
if root_metadata_ids != {fingerprints.identity.root_id}:
|
|
538
|
+
raise _fail(
|
|
539
|
+
"fingerprints.identity.root_id disagrees with the exact metadata artifact"
|
|
540
|
+
)
|
|
541
|
+
identity_metadata_names = {
|
|
542
|
+
f"CommonModule.{name}" for name in _PERMANENT_IDENTITY_RUNTIME_MODULE_NAMES
|
|
543
|
+
}
|
|
544
|
+
identity_metadata = {
|
|
545
|
+
item.name: item.object_id
|
|
546
|
+
for item in fingerprints.artifact.metadata
|
|
547
|
+
if item.name in identity_metadata_names
|
|
548
|
+
}
|
|
549
|
+
expected_identity_module_count = len(_PERMANENT_IDENTITY_RUNTIME_MODULE_NAMES)
|
|
550
|
+
identity_metadata_ids = tuple(identity_metadata.values())
|
|
551
|
+
identity_runtime_ids = fingerprints.identity.runtime_module_ids
|
|
552
|
+
if (
|
|
553
|
+
len(identity_metadata) != expected_identity_module_count
|
|
554
|
+
or set(identity_metadata) != identity_metadata_names
|
|
555
|
+
or len(set(identity_metadata_ids)) != expected_identity_module_count
|
|
556
|
+
or len(identity_runtime_ids) != expected_identity_module_count
|
|
557
|
+
or len(set(identity_runtime_ids)) != expected_identity_module_count
|
|
558
|
+
or set(identity_metadata_ids) != set(identity_runtime_ids)
|
|
559
|
+
):
|
|
560
|
+
raise _fail(
|
|
561
|
+
"fingerprints.identity.runtime_module_ids disagree with the exact metadata artifact"
|
|
562
|
+
)
|
|
563
|
+
if (
|
|
564
|
+
fingerprints.artifact.artifact_version != artifact_version
|
|
565
|
+
or fingerprints.artifact.protocol_version != protocol_version
|
|
566
|
+
):
|
|
567
|
+
raise _fail(
|
|
568
|
+
"manifest artifact fingerprint disagrees with the manifest versions"
|
|
569
|
+
)
|
|
570
|
+
if fingerprints.artifact.language_bound_by_name:
|
|
571
|
+
raise _fail("manifest artifact must not control the infobase language")
|
|
572
|
+
return ExtensionManifest(
|
|
573
|
+
schema_version=schema_version,
|
|
574
|
+
product_id=product_id,
|
|
575
|
+
extension_name=extension_name,
|
|
576
|
+
artifact_version=artifact_version,
|
|
577
|
+
protocol_version=protocol_version,
|
|
578
|
+
cfe_filename=cfe_filename,
|
|
579
|
+
cfe_size=_integer(root["cfe_size"], label="cfe_size"),
|
|
580
|
+
cfe_sha256=_hash(root["cfe_sha256"], label="cfe_sha256"),
|
|
581
|
+
fingerprints=fingerprints,
|
|
582
|
+
breakpoints=_parse_breakpoints(root["breakpoints"], fingerprints),
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
def _parse_xml(path: Path, *, label: str) -> ElementTree.Element:
|
|
587
|
+
try:
|
|
588
|
+
return ElementTree.parse(path).getroot()
|
|
589
|
+
except (OSError, ElementTree.ParseError) as error:
|
|
590
|
+
raise _fail(f"{label} is missing or malformed: {path}") from error
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
def _required_text(parent: ElementTree.Element, name: str, *, label: str) -> str:
|
|
594
|
+
value = parent.findtext(f"{{{_MD_NAMESPACE}}}{name}")
|
|
595
|
+
if value is None or not value:
|
|
596
|
+
raise _fail(f"{label}.{name} is missing")
|
|
597
|
+
return value
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
def _metadata_inventory(source_dir: Path) -> tuple[MetadataIdentity, ...]:
|
|
601
|
+
root = _parse_xml(source_dir / "ConfigDumpInfo.xml", label="ConfigDumpInfo.xml")
|
|
602
|
+
values: list[MetadataIdentity] = []
|
|
603
|
+
names: set[str] = set()
|
|
604
|
+
for index, item in enumerate(
|
|
605
|
+
root.findall(
|
|
606
|
+
f"{{{_DUMP_NAMESPACE}}}ConfigVersions/{{{_DUMP_NAMESPACE}}}Metadata"
|
|
607
|
+
)
|
|
608
|
+
):
|
|
609
|
+
name = item.attrib.get("name", "")
|
|
610
|
+
raw_id = item.attrib.get("id", "")
|
|
611
|
+
if not name or not raw_id:
|
|
612
|
+
raise _fail(f"ConfigDumpInfo.xml metadata entry {index} is incomplete")
|
|
613
|
+
if name in names:
|
|
614
|
+
raise _fail(f"ConfigDumpInfo.xml contains duplicate metadata name {name!r}")
|
|
615
|
+
names.add(name)
|
|
616
|
+
try:
|
|
617
|
+
object_id = UUID(raw_id.split(".", 1)[0])
|
|
618
|
+
except ValueError as error:
|
|
619
|
+
raise _fail(
|
|
620
|
+
f"ConfigDumpInfo.xml metadata {name!r} has an invalid UUID"
|
|
621
|
+
) from error
|
|
622
|
+
values.append(MetadataIdentity(name, object_id))
|
|
623
|
+
if not values:
|
|
624
|
+
raise _fail("ConfigDumpInfo.xml contains no metadata inventory")
|
|
625
|
+
return tuple(sorted(values, key=lambda value: value.name))
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
def _read_bsl(path: Path) -> str:
|
|
629
|
+
try:
|
|
630
|
+
return (
|
|
631
|
+
path.read_text(encoding="utf-8-sig")
|
|
632
|
+
.replace("\r\n", "\n")
|
|
633
|
+
.replace("\r", "\n")
|
|
634
|
+
)
|
|
635
|
+
except (OSError, UnicodeDecodeError) as error:
|
|
636
|
+
raise _fail(
|
|
637
|
+
f"runtime BSL module is missing or invalid UTF-8: {path}"
|
|
638
|
+
) from error
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
def _handshake(source: str, *, label: str) -> tuple[str, str, str]:
|
|
642
|
+
values: dict[str, str] = {}
|
|
643
|
+
for field, pattern in _HANDSHAKE_ASSIGNMENTS.items():
|
|
644
|
+
matches = [
|
|
645
|
+
match.group(1)
|
|
646
|
+
for line in source.splitlines()
|
|
647
|
+
if (match := pattern.fullmatch(line))
|
|
648
|
+
]
|
|
649
|
+
if len(matches) != 1:
|
|
650
|
+
raise _fail(f"{label} handshake must assign {field} exactly once")
|
|
651
|
+
values[field] = matches[0]
|
|
652
|
+
return values["product_id"], values["artifact_version"], values["protocol_version"]
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
def _marker_line(source: str, marker: str, *, label: str) -> int:
|
|
656
|
+
matches = [
|
|
657
|
+
line_number
|
|
658
|
+
for line_number, line in enumerate(source.splitlines(), start=1)
|
|
659
|
+
if marker in line
|
|
660
|
+
]
|
|
661
|
+
if len(matches) != 1:
|
|
662
|
+
raise _fail(f"{label} must contain exactly one {marker} marker")
|
|
663
|
+
return matches[0]
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
def _metadata_id(inventory: tuple[MetadataIdentity, ...], name: str) -> UUID:
|
|
667
|
+
matches = [item.object_id for item in inventory if item.name == name]
|
|
668
|
+
if len(matches) != 1:
|
|
669
|
+
raise _fail(f"ConfigDumpInfo.xml must contain metadata {name!r} exactly once")
|
|
670
|
+
return matches[0]
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
def fingerprint_extension_dump(
|
|
674
|
+
source_dir: Path,
|
|
675
|
+
*,
|
|
676
|
+
expected_product_id: str | None = PRODUCT_ID,
|
|
677
|
+
) -> ExtensionFingerprints:
|
|
678
|
+
configuration_root = _parse_xml(
|
|
679
|
+
source_dir / "Configuration.xml", label="Configuration.xml"
|
|
680
|
+
)
|
|
681
|
+
configuration = configuration_root.find(f"{{{_MD_NAMESPACE}}}Configuration")
|
|
682
|
+
if configuration is None:
|
|
683
|
+
raise _fail("Configuration.xml has no Configuration object")
|
|
684
|
+
properties = configuration.find(f"{{{_MD_NAMESPACE}}}Properties")
|
|
685
|
+
if properties is None:
|
|
686
|
+
raise _fail("Configuration.xml has no Configuration.Properties")
|
|
687
|
+
extension_name = _required_text(
|
|
688
|
+
properties, "Name", label="Configuration.Properties"
|
|
689
|
+
)
|
|
690
|
+
purpose = _required_text(
|
|
691
|
+
properties, "ConfigurationExtensionPurpose", label="Configuration.Properties"
|
|
692
|
+
)
|
|
693
|
+
keep_mapping = _required_text(
|
|
694
|
+
properties,
|
|
695
|
+
"KeepMappingToExtendedConfigurationObjectsByIDs",
|
|
696
|
+
label="Configuration.Properties",
|
|
697
|
+
)
|
|
698
|
+
name_prefix = _required_text(
|
|
699
|
+
properties, "NamePrefix", label="Configuration.Properties"
|
|
700
|
+
)
|
|
701
|
+
vendor = _required_text(properties, "Vendor", label="Configuration.Properties")
|
|
702
|
+
artifact_version = _required_text(
|
|
703
|
+
properties, "Version", label="Configuration.Properties"
|
|
704
|
+
)
|
|
705
|
+
belonging = _required_text(
|
|
706
|
+
properties, "ObjectBelonging", label="Configuration.Properties"
|
|
707
|
+
)
|
|
708
|
+
if extension_name != EXTENSION_NAME:
|
|
709
|
+
raise _fail(f"extension name must be {EXTENSION_NAME}")
|
|
710
|
+
if belonging == "Customization" or belonging != "Adopted":
|
|
711
|
+
raise _fail("configuration ObjectBelonging must be Adopted, not Customization")
|
|
712
|
+
if purpose != "AddOn":
|
|
713
|
+
raise _fail("configuration extension purpose must be AddOn")
|
|
714
|
+
if keep_mapping != "false":
|
|
715
|
+
raise _fail("configuration KeepMapping must be false")
|
|
716
|
+
if name_prefix != f"{EXTENSION_NAME}_":
|
|
717
|
+
raise _fail("configuration NamePrefix is invalid")
|
|
718
|
+
if expected_product_id is not None and vendor != expected_product_id:
|
|
719
|
+
raise _fail("configuration Vendor is invalid")
|
|
720
|
+
controlled_properties = (
|
|
721
|
+
"DefaultLanguage",
|
|
722
|
+
"InterfaceCompatibilityMode",
|
|
723
|
+
)
|
|
724
|
+
if any(
|
|
725
|
+
properties.find(f"{{{_MD_NAMESPACE}}}{name}") is not None
|
|
726
|
+
for name in controlled_properties
|
|
727
|
+
):
|
|
728
|
+
raise _fail(
|
|
729
|
+
"configuration must not control DefaultLanguage or "
|
|
730
|
+
"InterfaceCompatibilityMode"
|
|
731
|
+
)
|
|
732
|
+
try:
|
|
733
|
+
root_id = UUID(configuration.attrib["uuid"])
|
|
734
|
+
except (KeyError, ValueError) as error:
|
|
735
|
+
raise _fail("Configuration.xml root uuid must be a valid UUID") from error
|
|
736
|
+
|
|
737
|
+
inventory = _metadata_inventory(source_dir)
|
|
738
|
+
if _metadata_id(inventory, f"Configuration.{extension_name}") != root_id:
|
|
739
|
+
raise _fail("Configuration root UUID disagrees with ConfigDumpInfo.xml")
|
|
740
|
+
if any(item.name.startswith("Language.") for item in inventory):
|
|
741
|
+
raise _fail("ConfigDumpInfo.xml must not contain language metadata")
|
|
742
|
+
|
|
743
|
+
child_objects = configuration.find(f"{{{_MD_NAMESPACE}}}ChildObjects")
|
|
744
|
+
if child_objects is None:
|
|
745
|
+
raise _fail("Configuration.xml has no Configuration.ChildObjects")
|
|
746
|
+
permitted_child_tags = {f"{{{_MD_NAMESPACE}}}CommonModule"}
|
|
747
|
+
for child in child_objects:
|
|
748
|
+
if child.tag not in permitted_child_tags:
|
|
749
|
+
child_kind = child.tag.rsplit("}", 1)[-1]
|
|
750
|
+
raise _fail(
|
|
751
|
+
f"Configuration.ChildObjects child object kind {child_kind!r} is not permitted"
|
|
752
|
+
)
|
|
753
|
+
module_names = [
|
|
754
|
+
item.text
|
|
755
|
+
for item in child_objects.findall(f"{{{_MD_NAMESPACE}}}CommonModule")
|
|
756
|
+
if item.text
|
|
757
|
+
]
|
|
758
|
+
if len(set(module_names)) != len(module_names):
|
|
759
|
+
raise _fail("Configuration.xml contains duplicate CommonModule children")
|
|
760
|
+
missing_identity_modules = [
|
|
761
|
+
name
|
|
762
|
+
for name in _PERMANENT_IDENTITY_RUNTIME_MODULE_NAMES
|
|
763
|
+
if name not in module_names
|
|
764
|
+
]
|
|
765
|
+
if missing_identity_modules:
|
|
766
|
+
raise _fail(
|
|
767
|
+
"Configuration.xml is missing fixed identity runtime module(s): "
|
|
768
|
+
+ ", ".join(missing_identity_modules)
|
|
769
|
+
)
|
|
770
|
+
owned_module_ids = {
|
|
771
|
+
name: _metadata_id(inventory, f"CommonModule.{name}") for name in module_names
|
|
772
|
+
}
|
|
773
|
+
if len(set(owned_module_ids.values())) != len(owned_module_ids):
|
|
774
|
+
raise _fail("owned common modules must have distinct UUIDs")
|
|
775
|
+
runtime_module_ids = tuple(
|
|
776
|
+
sorted(
|
|
777
|
+
(
|
|
778
|
+
owned_module_ids[name]
|
|
779
|
+
for name in _PERMANENT_IDENTITY_RUNTIME_MODULE_NAMES
|
|
780
|
+
),
|
|
781
|
+
key=str,
|
|
782
|
+
)
|
|
783
|
+
)
|
|
784
|
+
|
|
785
|
+
managed_source = _read_bsl(source_dir / Path(_MANAGED_SOURCE))
|
|
786
|
+
server_source = _read_bsl(source_dir / Path(_SERVER_SOURCE))
|
|
787
|
+
managed_handshake = _handshake(managed_source, label="managed runtime module")
|
|
788
|
+
server_handshake = _handshake(server_source, label="server runtime module")
|
|
789
|
+
if managed_handshake != server_handshake:
|
|
790
|
+
raise _fail("managed and server runtime handshake assignments disagree")
|
|
791
|
+
if managed_handshake[0] != vendor:
|
|
792
|
+
raise _fail("runtime handshake product_id disagrees with Vendor")
|
|
793
|
+
if expected_product_id is not None and managed_handshake[0] != expected_product_id:
|
|
794
|
+
raise _fail("runtime handshake product_id is invalid")
|
|
795
|
+
if managed_handshake[1] != artifact_version:
|
|
796
|
+
raise _fail(
|
|
797
|
+
"runtime handshake artifact version disagrees with Configuration.xml"
|
|
798
|
+
)
|
|
799
|
+
protocol_version = managed_handshake[2]
|
|
800
|
+
|
|
801
|
+
source_hashes = tuple(
|
|
802
|
+
sorted(
|
|
803
|
+
(
|
|
804
|
+
(_MANAGED_SOURCE, sha256(managed_source.encode("utf-8")).hexdigest()),
|
|
805
|
+
(_SERVER_SOURCE, sha256(server_source.encode("utf-8")).hexdigest()),
|
|
806
|
+
)
|
|
807
|
+
)
|
|
808
|
+
)
|
|
809
|
+
identity = PermanentExtensionIdentity(
|
|
810
|
+
product_id=managed_handshake[0],
|
|
811
|
+
extension_name=extension_name,
|
|
812
|
+
root_id=root_id,
|
|
813
|
+
runtime_module_ids=runtime_module_ids,
|
|
814
|
+
purpose=purpose,
|
|
815
|
+
name_prefix=name_prefix,
|
|
816
|
+
vendor=vendor,
|
|
817
|
+
)
|
|
818
|
+
artifact = ExactExtensionArtifact(
|
|
819
|
+
artifact_version=artifact_version,
|
|
820
|
+
protocol_version=protocol_version,
|
|
821
|
+
language_bound_by_name=False,
|
|
822
|
+
metadata=inventory,
|
|
823
|
+
source_sha256=source_hashes,
|
|
824
|
+
)
|
|
825
|
+
return ExtensionFingerprints(
|
|
826
|
+
identity=identity,
|
|
827
|
+
artifact=artifact,
|
|
828
|
+
identity_sha256=_canonical_sha256(_identity_json(identity)),
|
|
829
|
+
artifact_sha256=_canonical_sha256(_artifact_json(artifact)),
|
|
830
|
+
)
|
|
831
|
+
|
|
832
|
+
|
|
833
|
+
def _verify_cfe(path: Path, manifest: ExtensionManifest) -> None:
|
|
834
|
+
try:
|
|
835
|
+
size = path.stat().st_size
|
|
836
|
+
except OSError as error:
|
|
837
|
+
raise _fail(f"extension CFE is unreadable: {path}") from error
|
|
838
|
+
if size != manifest.cfe_size:
|
|
839
|
+
raise _fail(f"extension CFE byte count is {size}, expected {manifest.cfe_size}")
|
|
840
|
+
digest = sha256()
|
|
841
|
+
try:
|
|
842
|
+
with path.open("rb") as stream:
|
|
843
|
+
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
|
|
844
|
+
digest.update(chunk)
|
|
845
|
+
except OSError as error:
|
|
846
|
+
raise _fail(f"extension CFE is unreadable: {path}") from error
|
|
847
|
+
if digest.hexdigest() != manifest.cfe_sha256:
|
|
848
|
+
raise _fail("extension CFE SHA-256 does not match the manifest")
|
|
849
|
+
|
|
850
|
+
|
|
851
|
+
def _fsync_copy(source: Path, destination: Path) -> None:
|
|
852
|
+
try:
|
|
853
|
+
with source.open("rb") as input_stream, destination.open("xb") as output_stream:
|
|
854
|
+
shutil.copyfileobj(input_stream, output_stream, length=1024 * 1024)
|
|
855
|
+
output_stream.flush()
|
|
856
|
+
os.fsync(output_stream.fileno())
|
|
857
|
+
except OSError as error:
|
|
858
|
+
raise _fail(
|
|
859
|
+
f"could not materialize packaged extension resource: {source}"
|
|
860
|
+
) from error
|
|
861
|
+
|
|
862
|
+
|
|
863
|
+
def _valid_cached_bundle(cache_dir: Path, manifest: ExtensionManifest) -> bool:
|
|
864
|
+
cfe_path = cache_dir / manifest.cfe_filename
|
|
865
|
+
cached_manifest_path = cache_dir / _MANIFEST_FILENAME
|
|
866
|
+
try:
|
|
867
|
+
cached = read_extension_manifest(cached_manifest_path)
|
|
868
|
+
_verify_cfe(cfe_path, cached)
|
|
869
|
+
except ExtensionBundleError:
|
|
870
|
+
return False
|
|
871
|
+
return cached == manifest
|
|
872
|
+
|
|
873
|
+
|
|
874
|
+
def materialize_extension_bundle(
|
|
875
|
+
cfe: Path, manifest: Path, runtime_dir: Path
|
|
876
|
+
) -> ExtensionBundle:
|
|
877
|
+
parsed = read_extension_manifest(manifest)
|
|
878
|
+
if cfe.name != parsed.cfe_filename:
|
|
879
|
+
raise _fail(
|
|
880
|
+
f"extension CFE filename {cfe.name!r} does not match manifest filename {parsed.cfe_filename!r}"
|
|
881
|
+
)
|
|
882
|
+
_verify_cfe(cfe, parsed)
|
|
883
|
+
cache_parent = runtime_dir / "cache" / "extensions"
|
|
884
|
+
cache_dir = cache_parent / parsed.cfe_sha256
|
|
885
|
+
if cache_dir.is_dir():
|
|
886
|
+
if not _valid_cached_bundle(cache_dir, parsed):
|
|
887
|
+
raise _fail(f"extension bundle cache entry is corrupt: {cache_dir}")
|
|
888
|
+
return ExtensionBundle(cache_dir / parsed.cfe_filename, parsed, cache_dir)
|
|
889
|
+
|
|
890
|
+
cache_parent.mkdir(parents=True, exist_ok=True)
|
|
891
|
+
temporary = Path(
|
|
892
|
+
tempfile.mkdtemp(prefix=f".{parsed.cfe_sha256}-", dir=cache_parent)
|
|
893
|
+
)
|
|
894
|
+
try:
|
|
895
|
+
staged_cfe = temporary / parsed.cfe_filename
|
|
896
|
+
staged_manifest = temporary / _MANIFEST_FILENAME
|
|
897
|
+
_fsync_copy(cfe, staged_cfe)
|
|
898
|
+
_fsync_copy(manifest, staged_manifest)
|
|
899
|
+
staged_parsed = read_extension_manifest(staged_manifest)
|
|
900
|
+
_verify_cfe(staged_cfe, staged_parsed)
|
|
901
|
+
if staged_parsed != parsed:
|
|
902
|
+
raise _fail("materialized extension manifest changed during copying")
|
|
903
|
+
try:
|
|
904
|
+
os.replace(temporary, cache_dir)
|
|
905
|
+
except OSError as error:
|
|
906
|
+
if not cache_dir.is_dir() or not _valid_cached_bundle(cache_dir, parsed):
|
|
907
|
+
raise _fail(
|
|
908
|
+
f"could not atomically publish extension bundle: {cache_dir}"
|
|
909
|
+
) from error
|
|
910
|
+
return ExtensionBundle(cache_dir / parsed.cfe_filename, parsed, cache_dir)
|
|
911
|
+
finally:
|
|
912
|
+
if temporary.exists():
|
|
913
|
+
shutil.rmtree(temporary)
|
|
914
|
+
|
|
915
|
+
|
|
916
|
+
def packaged_extension_bundle(runtime_dir: Path) -> ExtensionBundle:
|
|
917
|
+
root = importlib.resources.files("onec_runtime").joinpath("resources", "extension")
|
|
918
|
+
try:
|
|
919
|
+
with importlib.resources.as_file(root) as resource_root:
|
|
920
|
+
return materialize_extension_bundle(
|
|
921
|
+
resource_root / f"{EXTENSION_NAME}.cfe",
|
|
922
|
+
resource_root / _MANIFEST_FILENAME,
|
|
923
|
+
runtime_dir,
|
|
924
|
+
)
|
|
925
|
+
except (FileNotFoundError, IsADirectoryError, NotADirectoryError) as error:
|
|
926
|
+
raise _fail("packaged runtime extension resources are missing") from error
|
|
927
|
+
|
|
928
|
+
|
|
929
|
+
__all__ = [
|
|
930
|
+
"EXTENSION_NAME",
|
|
931
|
+
"PRODUCT_ID",
|
|
932
|
+
"BreakpointContract",
|
|
933
|
+
"ExactExtensionArtifact",
|
|
934
|
+
"ExtensionBundle",
|
|
935
|
+
"ExtensionFingerprints",
|
|
936
|
+
"ExtensionHandshakeEvidence",
|
|
937
|
+
"ExtensionManifest",
|
|
938
|
+
"MetadataIdentity",
|
|
939
|
+
"PermanentExtensionIdentity",
|
|
940
|
+
"fingerprint_extension_dump",
|
|
941
|
+
"materialize_extension_bundle",
|
|
942
|
+
"packaged_extension_bundle",
|
|
943
|
+
"read_extension_manifest",
|
|
944
|
+
]
|