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.
Files changed (87) hide show
  1. onec_interactive_runtime_core-0.1.19.dist-info/METADATA +12 -0
  2. onec_interactive_runtime_core-0.1.19.dist-info/RECORD +87 -0
  3. onec_interactive_runtime_core-0.1.19.dist-info/WHEEL +4 -0
  4. onec_interactive_runtime_core-0.1.19.dist-info/licenses/COPYRIGHT +9 -0
  5. onec_interactive_runtime_core-0.1.19.dist-info/licenses/LICENSE +232 -0
  6. onec_interactive_runtime_core-0.1.19.dist-info/licenses/THIRD_PARTY_NOTICES.txt +32 -0
  7. onec_runtime/AGENTS.md +7 -0
  8. onec_runtime/__init__.py +3 -0
  9. onec_runtime/artifacts.py +59 -0
  10. onec_runtime/bootstrap.py +310 -0
  11. onec_runtime/breakpoint_workspace.py +249 -0
  12. onec_runtime/bsl/__init__.py +139 -0
  13. onec_runtime/bsl/diagnostics.py +1046 -0
  14. onec_runtime/bsl/full_ast_worker_projection.py +522 -0
  15. onec_runtime/bsl/generated_semantic_parser.py +6322 -0
  16. onec_runtime/bsl/lexer.py +164 -0
  17. onec_runtime/bsl/module_catalog.py +420 -0
  18. onec_runtime/bsl/module_delta.py +1079 -0
  19. onec_runtime/bsl/module_universe.py +1877 -0
  20. onec_runtime/bsl/notebook_cells.py +232 -0
  21. onec_runtime/bsl/notebook_method_globals.py +74 -0
  22. onec_runtime/bsl/notebook_methods.py +175 -0
  23. onec_runtime/bsl/parser_artifact_identity.py +119 -0
  24. onec_runtime/bsl/parser_target.py +388 -0
  25. onec_runtime/bsl/preprocessor.py +16 -0
  26. onec_runtime/bsl/semantic_lowering.py +1539 -0
  27. onec_runtime/bsl/source_maps.py +2411 -0
  28. onec_runtime/bsl/worker_dependency_resolver.py +289 -0
  29. onec_runtime/bsl/worker_preprocessor.py +247 -0
  30. onec_runtime/bsl/worker_projection_model.py +137 -0
  31. onec_runtime/bsl/worker_reload_source_map.py +1697 -0
  32. onec_runtime/capture.py +113 -0
  33. onec_runtime/capture_source.py +453 -0
  34. onec_runtime/compact_table.py +376 -0
  35. onec_runtime/compact_table_backend.py +500 -0
  36. onec_runtime/config.py +198 -0
  37. onec_runtime/configurator_agent.py +520 -0
  38. onec_runtime/controller_worker.py +400 -0
  39. onec_runtime/credentials.py +18 -0
  40. onec_runtime/epf_container.py +196 -0
  41. onec_runtime/errors.py +205 -0
  42. onec_runtime/experiment.py +130 -0
  43. onec_runtime/extension_bundle.py +944 -0
  44. onec_runtime/extension_lifecycle.py +402 -0
  45. onec_runtime/extension_state.py +222 -0
  46. onec_runtime/fault_injection.py +30 -0
  47. onec_runtime/kernel.py +179 -0
  48. onec_runtime/lease.py +123 -0
  49. onec_runtime/observation.py +287 -0
  50. onec_runtime/performance_profile.py +245 -0
  51. onec_runtime/privacy.py +263 -0
  52. onec_runtime/processes.py +265 -0
  53. onec_runtime/prototype_runtime.py +2644 -0
  54. onec_runtime/rdbg/__init__.py +1 -0
  55. onec_runtime/rdbg/models.py +130 -0
  56. onec_runtime/rdbg/reconnect.py +59 -0
  57. onec_runtime/rdbg/session.py +913 -0
  58. onec_runtime/rdbg/transport.py +138 -0
  59. onec_runtime/rdbg/xml_codec.py +980 -0
  60. onec_runtime/recovery.py +106 -0
  61. onec_runtime/recovery_journal.py +79 -0
  62. onec_runtime/resources/extension/OnecInteractiveRuntime.cfe +0 -0
  63. onec_runtime/resources/extension/extension-manifest.json +108 -0
  64. onec_runtime/runtime_api.py +5272 -0
  65. onec_runtime/runtime_contracts.py +196 -0
  66. onec_runtime/server_worker.py +2034 -0
  67. onec_runtime/session.py +2123 -0
  68. onec_runtime/startup_diagnostics.py +45 -0
  69. onec_runtime/stop_routing.py +65 -0
  70. onec_runtime/supervised_processes.py +232 -0
  71. onec_runtime/supervised_runtime.py +512 -0
  72. onec_runtime/supervisor.py +363 -0
  73. onec_runtime/supervisor_model.py +148 -0
  74. onec_runtime/supervisor_protocol.py +77 -0
  75. onec_runtime/table_materialization.py +471 -0
  76. onec_runtime/table_transfer_backend.py +203 -0
  77. onec_runtime/table_value.py +311 -0
  78. onec_runtime/toolchain.py +730 -0
  79. onec_runtime/value_materialization.py +432 -0
  80. onec_runtime/value_transfer_backend.py +199 -0
  81. onec_runtime/worker_breakpoints.py +1431 -0
  82. onec_runtime/worker_epf.py +313 -0
  83. onec_runtime/worker_epf_template.py +53 -0
  84. onec_runtime/worker_stage_protocol.py +516 -0
  85. onec_runtime/worker_universe.py +3772 -0
  86. onec_runtime_build/__init__.py +1 -0
  87. 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
+ ]