iricity-frontier-shared 0.5.0__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.
@@ -0,0 +1,128 @@
1
+ """Frontier State Protocol primitives, Python implementation.
2
+
3
+ Byte-for-byte compatible with the JavaScript reference (`@iricity/frontier-shared`),
4
+ proven by the shared conformance vectors in ``frontier_shared/vectors``.
5
+ """
6
+
7
+ from .base58 import base58_decode, base58_encode, multibase_decode_base58btc, multibase_encode_base58btc
8
+ from .canonical import MAX_DEPTH, canonical_json, format_number
9
+ from .credential import (
10
+ AUTHORIZATION_CREDENTIAL_TYPE,
11
+ AUTHORIZATION_VOCAB,
12
+ CREDENTIAL_CONTEXT_V2,
13
+ CREDENTIAL_SUBJECT_FIELDS,
14
+ CRYPTOSUITE,
15
+ DATA_INTEGRITY_PROOF,
16
+ PROOF_PURPOSE,
17
+ credential_hash_data,
18
+ credential_subject_from_authorization,
19
+ issue_authorization_credential,
20
+ verify_authorization_credential,
21
+ verify_credential,
22
+ )
23
+ from .envelope import (
24
+ ENVELOPE_DOMAIN_TAG,
25
+ ENVELOPE_VERSION,
26
+ TIMESTAMP_PATTERN,
27
+ create_envelope,
28
+ envelope_hash,
29
+ envelope_header,
30
+ envelope_id_from_hash,
31
+ envelope_signing_input,
32
+ is_timestamp,
33
+ verify_envelope,
34
+ )
35
+ from .hashing import HASH_PREFIX, hash_bytes, hash_payload, hash_record, hash_text, is_hash_ref, sha256_hex, sha256_hex_bytes
36
+ from .keys import (
37
+ ED25519_MULTICODEC_PREFIX,
38
+ KeyPair,
39
+ decode_signature,
40
+ did_from_public_key_bytes,
41
+ encode_signature,
42
+ fingerprint_did,
43
+ is_did_key,
44
+ is_valid_signature_text,
45
+ public_key_bytes_from_did,
46
+ public_key_multibase_from_did,
47
+ resolve_signer,
48
+ sign_bytes,
49
+ verify_bytes,
50
+ )
51
+ from .receipt import (
52
+ RECEIPT_DOMAIN_TAG,
53
+ RECEIPT_SIGNATURE_ALGORITHMS,
54
+ RECEIPT_VERSION,
55
+ compute_receipt_hash,
56
+ create_receipt_record,
57
+ derive_receipt_id,
58
+ receipt_core,
59
+ receipt_signing_input,
60
+ verify_receipt_chain,
61
+ verify_receipt_record,
62
+ )
63
+
64
+ __version__ = "0.5.0"
65
+
66
+ __all__ = [
67
+ "__version__",
68
+ "issue_authorization_credential",
69
+ "verify_credential",
70
+ "verify_authorization_credential",
71
+ "credential_hash_data",
72
+ "credential_subject_from_authorization",
73
+ "CREDENTIAL_CONTEXT_V2",
74
+ "AUTHORIZATION_VOCAB",
75
+ "AUTHORIZATION_CREDENTIAL_TYPE",
76
+ "CREDENTIAL_SUBJECT_FIELDS",
77
+ "CRYPTOSUITE",
78
+ "DATA_INTEGRITY_PROOF",
79
+ "PROOF_PURPOSE",
80
+ "canonical_json",
81
+ "format_number",
82
+ "MAX_DEPTH",
83
+ "hash_text",
84
+ "is_valid_signature_text",
85
+ "is_timestamp",
86
+ "TIMESTAMP_PATTERN",
87
+ "envelope_id_from_hash",
88
+ "derive_receipt_id",
89
+ "HASH_PREFIX",
90
+ "sha256_hex",
91
+ "sha256_hex_bytes",
92
+ "hash_payload",
93
+ "hash_bytes",
94
+ "hash_record",
95
+ "is_hash_ref",
96
+ "base58_encode",
97
+ "base58_decode",
98
+ "multibase_encode_base58btc",
99
+ "multibase_decode_base58btc",
100
+ "ED25519_MULTICODEC_PREFIX",
101
+ "KeyPair",
102
+ "resolve_signer",
103
+ "did_from_public_key_bytes",
104
+ "public_key_bytes_from_did",
105
+ "public_key_multibase_from_did",
106
+ "is_did_key",
107
+ "fingerprint_did",
108
+ "sign_bytes",
109
+ "verify_bytes",
110
+ "encode_signature",
111
+ "decode_signature",
112
+ "ENVELOPE_VERSION",
113
+ "ENVELOPE_DOMAIN_TAG",
114
+ "create_envelope",
115
+ "verify_envelope",
116
+ "envelope_header",
117
+ "envelope_hash",
118
+ "envelope_signing_input",
119
+ "RECEIPT_VERSION",
120
+ "RECEIPT_DOMAIN_TAG",
121
+ "RECEIPT_SIGNATURE_ALGORITHMS",
122
+ "create_receipt_record",
123
+ "verify_receipt_record",
124
+ "verify_receipt_chain",
125
+ "receipt_core",
126
+ "receipt_signing_input",
127
+ "compute_receipt_hash",
128
+ ]
@@ -0,0 +1,47 @@
1
+ """Base58btc (Bitcoin alphabet), as used by multibase prefix 'z'."""
2
+
3
+ from __future__ import annotations
4
+
5
+ __all__ = ["base58_encode", "base58_decode", "multibase_encode_base58btc", "multibase_decode_base58btc"]
6
+
7
+ ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
8
+ _INDEX = {char: index for index, char in enumerate(ALPHABET)}
9
+
10
+
11
+ def base58_encode(data: bytes) -> str:
12
+ data = bytes(data)
13
+ if not data:
14
+ return ""
15
+ value = int.from_bytes(data, "big")
16
+ encoded = ""
17
+ while value > 0:
18
+ value, remainder = divmod(value, 58)
19
+ encoded = ALPHABET[remainder] + encoded
20
+ leading = len(data) - len(data.lstrip(b"\x00"))
21
+ return "1" * leading + encoded
22
+
23
+
24
+ def base58_decode(text: str) -> bytes:
25
+ if not isinstance(text, str):
26
+ raise TypeError("base58_decode expects a string")
27
+ if text == "":
28
+ return b""
29
+ value = 0
30
+ for char in text:
31
+ digit = _INDEX.get(char)
32
+ if digit is None:
33
+ raise ValueError(f"base58_decode: invalid character {char!r}")
34
+ value = value * 58 + digit
35
+ body = b"" if value == 0 else value.to_bytes((value.bit_length() + 7) // 8, "big")
36
+ leading = len(text) - len(text.lstrip("1"))
37
+ return b"\x00" * leading + body
38
+
39
+
40
+ def multibase_encode_base58btc(data: bytes) -> str:
41
+ return "z" + base58_encode(data)
42
+
43
+
44
+ def multibase_decode_base58btc(text: str) -> bytes:
45
+ if not isinstance(text, str) or not text.startswith("z"):
46
+ raise ValueError('expected a multibase base58btc string starting with "z"')
47
+ return base58_decode(text[1:])
@@ -0,0 +1,131 @@
1
+ """Canonical JSON per RFC 8785 (JSON Canonicalization Scheme).
2
+
3
+ Rules implemented:
4
+ - object members sorted by the UTF-16 code units of their names
5
+ - numbers serialized with the ECMAScript Number-to-string algorithm; every number
6
+ is a double, so integers beyond 2**53 are rounded exactly as JavaScript rounds them
7
+ - strings escaped exactly as ECMAScript JSON.stringify escapes them
8
+ - no insignificant whitespace
9
+
10
+ Rejected, because a canonical form must be unambiguous and a verifier must not be
11
+ crashable: non-finite floats, non-string keys, non-JSON values, nesting deeper than
12
+ MAX_DEPTH.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import math
18
+ from decimal import Decimal
19
+ from typing import Any
20
+
21
+ __all__ = ["canonical_json", "format_number", "MAX_DEPTH"]
22
+
23
+ MAX_DEPTH = 256
24
+ _MAX_SAFE_INTEGER = 2**53
25
+
26
+
27
+ def format_number(value: float | int) -> str:
28
+ """ECMAScript Number::toString for a finite number (ECMA-262 section 6.1.6.1.20)."""
29
+ if isinstance(value, bool):
30
+ raise TypeError("booleans are not numbers")
31
+ if isinstance(value, int):
32
+ if abs(value) < _MAX_SAFE_INTEGER:
33
+ return str(value)
34
+ # JSON has only doubles: a larger integer literal is the nearest double.
35
+ value = float(value)
36
+ if not isinstance(value, float):
37
+ raise TypeError(f"not a number: {type(value).__name__}")
38
+ if math.isnan(value) or math.isinf(value):
39
+ raise ValueError("canonical_json: non-finite number")
40
+ if value == 0.0:
41
+ return "0"
42
+
43
+ sign = "-" if value < 0 else ""
44
+ # repr() gives the shortest digit string that round-trips, the same digits
45
+ # ECMAScript chooses. Decimal exposes them as (digits, exponent).
46
+ tup = Decimal(repr(abs(value))).as_tuple()
47
+ digits = list(tup.digits)
48
+ exponent = int(tup.exponent)
49
+ while len(digits) > 1 and digits[-1] == 0:
50
+ digits.pop()
51
+ exponent += 1
52
+ k = len(digits)
53
+ n = k + exponent # value = 0.d1d2...dk x 10^n
54
+ s = "".join(str(d) for d in digits)
55
+
56
+ if k <= n <= 21:
57
+ body = s + "0" * (n - k)
58
+ elif 0 < n <= 21:
59
+ body = s[:n] + "." + s[n:]
60
+ elif -6 < n <= 0:
61
+ body = "0." + "0" * (-n) + s
62
+ else:
63
+ e = n - 1
64
+ exp = ("+" if e >= 0 else "-") + str(abs(e))
65
+ body = (s if k == 1 else s[0] + "." + s[1:]) + "e" + exp
66
+ return sign + body
67
+
68
+
69
+ _SHORT_ESCAPES = {
70
+ '"': '\\"',
71
+ "\\": "\\\\",
72
+ "\b": "\\b",
73
+ "\f": "\\f",
74
+ "\n": "\\n",
75
+ "\r": "\\r",
76
+ "\t": "\\t",
77
+ }
78
+
79
+
80
+ def _escape_string(text: str) -> str:
81
+ out = ['"']
82
+ for ch in text:
83
+ esc = _SHORT_ESCAPES.get(ch)
84
+ if esc is not None:
85
+ out.append(esc)
86
+ continue
87
+ code = ord(ch)
88
+ if code < 0x20 or 0xD800 <= code <= 0xDFFF:
89
+ out.append("\\u%04x" % code)
90
+ else:
91
+ out.append(ch)
92
+ out.append('"')
93
+ return "".join(out)
94
+
95
+
96
+ def _key_order(name: Any) -> bytes:
97
+ # Lexicographic order of UTF-16BE bytes equals UTF-16 code unit order.
98
+ if not isinstance(name, str):
99
+ raise TypeError("canonical_json: object keys must be strings")
100
+ return name.encode("utf-16-be", "surrogatepass")
101
+
102
+
103
+ def _serialize(value: Any, path: str, depth: int) -> str:
104
+ if depth > MAX_DEPTH:
105
+ raise TypeError(f"canonical_json: nesting deeper than {MAX_DEPTH} at {path}")
106
+ if value is None:
107
+ return "null"
108
+ if value is True:
109
+ return "true"
110
+ if value is False:
111
+ return "false"
112
+ if isinstance(value, str):
113
+ return _escape_string(value)
114
+ if isinstance(value, (int, float)):
115
+ try:
116
+ return format_number(value)
117
+ except ValueError as error:
118
+ raise ValueError(f"{error} at {path}") from None
119
+ if isinstance(value, (list, tuple)):
120
+ return "[" + ",".join(_serialize(item, f"{path}[{index}]", depth + 1) for index, item in enumerate(value)) + "]"
121
+ if isinstance(value, dict):
122
+ members = []
123
+ for key in sorted(value.keys(), key=_key_order):
124
+ members.append(_escape_string(key) + ":" + _serialize(value[key], f"{path}.{key}", depth + 1))
125
+ return "{" + ",".join(members) + "}"
126
+ raise TypeError(f"canonical_json: unsupported value {type(value).__name__} at {path}")
127
+
128
+
129
+ def canonical_json(value: Any) -> str:
130
+ """Return the RFC 8785 canonical JSON text of a JSON-compatible Python value."""
131
+ return _serialize(value, "$", 0)
@@ -0,0 +1,267 @@
1
+ """Conformance runner: prove this implementation reproduces the published vectors byte for byte.
2
+
3
+ Usage::
4
+
5
+ frontier-shared-conformance # bundled vectors
6
+ frontier-shared-conformance path/to/vectors.json
7
+ python -m frontier_shared.conformance
8
+
9
+ An implementation conforms only when every suite passes and none is skipped.
10
+ Exit status 0 when conformant, 1 otherwise.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ import sys
17
+ from importlib import resources
18
+ from pathlib import Path
19
+ from typing import Any
20
+
21
+ from .base58 import base58_decode, base58_encode
22
+ from .canonical import canonical_json
23
+ from .envelope import create_envelope, verify_envelope
24
+ from .hashing import hash_payload
25
+ from .keys import KeyPair, did_from_public_key_bytes, public_key_bytes_from_did
26
+ from .receipt import create_receipt_record, verify_receipt_record
27
+ from .credential import credential_hash_data, issue_authorization_credential, verify_authorization_credential, verify_credential
28
+
29
+ __all__ = ["VECTORS_VERSION", "SUITES", "load_vectors", "bundled_vectors_path", "bundled_lock_path", "run_conformance", "format_report", "main"]
30
+
31
+ VECTORS_VERSION = "3"
32
+ SUITES = ["canonical_json", "hash_payload", "base58btc", "did_key", "ed25519", "envelope", "receipt", "credential", "rejections"]
33
+
34
+
35
+ def bundled_vectors_path() -> Path:
36
+ return Path(str(resources.files("frontier_shared") / "vectors" / f"v{VECTORS_VERSION}" / "vectors.json"))
37
+
38
+
39
+ def bundled_lock_path() -> Path:
40
+ return Path(str(resources.files("frontier_shared") / "vectors" / f"v{VECTORS_VERSION}" / "vectors.lock.json"))
41
+
42
+
43
+ def load_vectors(path: str | Path | None = None) -> dict:
44
+ target = Path(path) if path else bundled_vectors_path()
45
+ with open(target, "r", encoding="utf-8") as handle:
46
+ return json.load(handle)
47
+
48
+
49
+ def _signer(v: dict) -> dict:
50
+ return {"private_key_seed_hex": v["signer_seed_hex"]}
51
+
52
+
53
+ def run_conformance(vectors: dict | None = None) -> dict:
54
+ if vectors is None:
55
+ vectors = load_vectors()
56
+ results: list[dict] = []
57
+
58
+ def record(suite: str, case_id: str, status: str, detail: str | None = None) -> None:
59
+ entry = {"suite": suite, "id": case_id, "status": status}
60
+ if detail:
61
+ entry["detail"] = detail
62
+ results.append(entry)
63
+
64
+ def check(suite: str, case_id: str, fn) -> None:
65
+ try:
66
+ outcome = fn()
67
+ if outcome is True:
68
+ record(suite, case_id, "passed")
69
+ else:
70
+ record(suite, case_id, "failed", outcome if isinstance(outcome, str) else "mismatch")
71
+ except Exception as error: # noqa: BLE001 - every failure is a finding
72
+ record(suite, case_id, "failed", f"{type(error).__name__}: {error}")
73
+
74
+ def suite(name: str) -> list:
75
+ cases = vectors.get(name)
76
+ if not isinstance(cases, list) or not cases:
77
+ record(name, "*", "failed", f'vectors file has no "{name}" suite')
78
+ return []
79
+ return cases
80
+
81
+ for v in suite("canonical_json"):
82
+ def canonical_case(v=v):
83
+ actual = canonical_json(v["input"])
84
+ if actual != v["expected_canonical"]:
85
+ return f"expected {v['expected_canonical']} got {actual}"
86
+ digest = hash_payload(v["input"])
87
+ return True if digest == v["expected_hash"] else f"hash mismatch: {digest}"
88
+ check("canonical_json", v["id"], canonical_case)
89
+
90
+ for v in suite("hash_payload"):
91
+ def hash_case(v=v):
92
+ actual = hash_payload(v["input"])
93
+ return True if actual == v["expected_hash"] else f"expected {v['expected_hash']} got {actual}"
94
+ check("hash_payload", v["id"], hash_case)
95
+
96
+ for v in suite("base58btc"):
97
+ def base58_case(v=v):
98
+ encoded = base58_encode(bytes.fromhex(v["input_hex"]))
99
+ if encoded != v["expected"]:
100
+ return f"encode: expected {v['expected']} got {encoded}"
101
+ decoded = base58_decode(v["expected"]).hex()
102
+ return True if decoded == v["input_hex"] else f"decode: expected {v['input_hex']} got {decoded}"
103
+ check("base58btc", v.get("id") or v["input_hex"] or "empty", base58_case)
104
+
105
+ for v in suite("did_key"):
106
+ def did_case(v=v):
107
+ did = did_from_public_key_bytes(bytes.fromhex(v["public_key_hex"]))
108
+ if did != v["expected_did"]:
109
+ return f"encode: expected {v['expected_did']} got {did}"
110
+ raw = public_key_bytes_from_did(v["expected_did"]).hex()
111
+ return True if raw == v["public_key_hex"] else f"decode: expected {v['public_key_hex']} got {raw}"
112
+ check("did_key", v["expected_did"], did_case)
113
+
114
+ for v in suite("ed25519"):
115
+ def ed25519_case(v=v):
116
+ pair = KeyPair.from_seed(v["seed_hex"])
117
+ if pair.public_key_bytes.hex() != v["public_key_hex"]:
118
+ return "public key mismatch"
119
+ actual = pair.sign(bytes.fromhex(v["message_hex"])).hex()
120
+ return True if actual == v["expected_signature_hex"] else f"signature mismatch: {actual}"
121
+ check("ed25519", v["id"], ed25519_case)
122
+
123
+ for v in suite("envelope"):
124
+ def envelope_case(v=v):
125
+ detached = bool(v.get("detached"))
126
+ env = create_envelope(
127
+ payload=v["payload"],
128
+ payload_type=v["payload_type"],
129
+ signer=_signer(v),
130
+ created_at=v["created_at"],
131
+ previous_envelope_hash=v["previous_envelope_hash"],
132
+ detached=detached,
133
+ )
134
+ e = v["expected"]
135
+ for field in ("signer", "payload_hash", "envelope_hash", "envelope_id"):
136
+ if env[field] != e[field]:
137
+ return f"{field}: expected {e[field]} got {env[field]}"
138
+ if env["signature"]["value"] != e["signature_value"]:
139
+ return f"signature: {env['signature']['value']}"
140
+ if detached and "payload" in env:
141
+ return "detached envelope must not carry a payload"
142
+ verification = verify_envelope(env, payload=v["payload"]) if detached else verify_envelope(env)
143
+ if not verification["verified"]:
144
+ return "self-verification failed: " + "; ".join(verification["errors"])
145
+ if detached and not verify_envelope(env)["verified"]:
146
+ return "detached envelope must verify without its payload"
147
+ return True
148
+ check("envelope", v["id"], envelope_case)
149
+
150
+ for v in suite("receipt"):
151
+ def receipt_case(v=v):
152
+ if v.get("legacy"):
153
+ verification = verify_receipt_record(v["receipt"], record=v["record"])
154
+ if verification["verified"] != v["expected"]["verified"]:
155
+ return f"legacy: expected verified={v['expected']['verified']} got {verification['verified']}: " + "; ".join(verification["errors"])
156
+ return True
157
+ issuer: Any = _signer(v) if v.get("signer_seed_hex") else v["issuer"]
158
+ receipt = create_receipt_record(record=v["record"], previous_hash=v["previous_hash"], issuer=issuer, timestamp=v["timestamp"])
159
+ e = v["expected"]
160
+ for field in ("receipt_id", "record_hash", "previous_hash", "receipt_hash", "issuer", "signature", "signature_algorithm", "verification_status"):
161
+ if receipt.get(field) != e.get(field):
162
+ return f"{field}: expected {e.get(field)} got {receipt.get(field)}"
163
+ verification = verify_receipt_record(receipt, record=v["record"])
164
+ return True if verification["verified"] else "self-verification failed: " + "; ".join(verification["errors"])
165
+ check("receipt", v["id"], receipt_case)
166
+
167
+ for v in suite("credential"):
168
+ def credential_case(v=v):
169
+ if v.get("authorization_record"):
170
+ bound = verify_authorization_credential(v["authorization_record"], at=v["at"])
171
+ if bound["verified"] != v["expected"]["verified"]:
172
+ return f"expected verified={v['expected']['verified']}: " + "; ".join(bound["errors"])
173
+ if bound["issuer"] != v["expected"]["issuer"]:
174
+ return f"issuer: expected {v['expected']['issuer']} got {bound['issuer']}"
175
+ return True
176
+ credential = issue_authorization_credential(v["authorization"], _signer(v), created=v["created"], id=v.get("credential_id"))
177
+ e = v["expected"]
178
+ if credential["id"] != e["id"]:
179
+ return f"id: expected {e['id']} got {credential['id']}"
180
+ if credential["issuer"] != e["issuer"]:
181
+ return f"issuer: expected {e['issuer']} got {credential['issuer']}"
182
+ if credential["proof"]["verificationMethod"] != e["verification_method"]:
183
+ return f"verificationMethod: {credential['proof']['verificationMethod']}"
184
+ if credential["proof"]["proofValue"] != e["proof_value"]:
185
+ return f"proofValue: expected {e['proof_value']} got {credential['proof']['proofValue']}"
186
+ hash_data = credential_hash_data(credential, credential["proof"]).hex()
187
+ if hash_data != e["hash_data_hex"]:
188
+ return f"hash_data: expected {e['hash_data_hex']} got {hash_data}"
189
+ if canonical_json(credential["credentialSubject"]) != canonical_json(e["credential_subject"]):
190
+ return "credentialSubject differs"
191
+ verification = verify_credential(credential, at=v["at"])
192
+ return True if verification["verified"] else "self-verification failed: " + "; ".join(verification["errors"])
193
+ check("credential", v["id"], credential_case)
194
+
195
+ for v in suite("rejections"):
196
+ def rejection_case(v=v):
197
+ options = v.get("options") or {}
198
+ if v["kind"] == "credential":
199
+ kwargs = {}
200
+ if "at" in options:
201
+ kwargs["at"] = options["at"]
202
+ if "expected_issuer" in options:
203
+ kwargs["expected_issuer"] = options["expected_issuer"]
204
+ verification = verify_credential(v["object"], **kwargs)
205
+ return True if verification["verified"] is False else "tampered credential was accepted"
206
+ if v["kind"] == "authorization":
207
+ verification = verify_authorization_credential(v["object"], at=options.get("at"))
208
+ return True if verification["verified"] is False else "unbound authorization was accepted"
209
+ if v["kind"] == "envelope":
210
+ kwargs = {}
211
+ if "payload" in options:
212
+ kwargs["payload"] = options["payload"]
213
+ if "expected_signer" in options:
214
+ kwargs["expected_signer"] = options["expected_signer"]
215
+ if "allowed_payload_types" in options:
216
+ kwargs["allowed_payload_types"] = options["allowed_payload_types"]
217
+ verification = verify_envelope(v["object"], **kwargs)
218
+ return True if verification["verified"] is False else "tampered envelope was accepted"
219
+ if v["kind"] == "receipt":
220
+ kwargs = {}
221
+ if "record" in options:
222
+ kwargs["record"] = options["record"]
223
+ if "previous_receipt" in options:
224
+ kwargs["previous_receipt"] = options["previous_receipt"]
225
+ if "expected_issuer" in options:
226
+ kwargs["expected_issuer"] = options["expected_issuer"]
227
+ verification = verify_receipt_record(v["object"], **kwargs)
228
+ return True if verification["verified"] is False else "tampered receipt was accepted"
229
+ return f"unknown rejection kind {v['kind']}"
230
+ check("rejections", v["id"], rejection_case)
231
+
232
+ passed = sum(1 for r in results if r["status"] == "passed")
233
+ failed = sum(1 for r in results if r["status"] == "failed")
234
+ skipped = sum(1 for r in results if r["status"] == "skipped")
235
+ return {
236
+ "vectors_version": vectors.get("vectors_version"),
237
+ "conformant": failed == 0 and skipped == 0 and passed > 0,
238
+ "passed": passed,
239
+ "failed": failed,
240
+ "skipped": skipped,
241
+ "results": results,
242
+ }
243
+
244
+
245
+ def format_report(report: dict) -> str:
246
+ lines = [
247
+ f"conformance vectors {report['vectors_version']}: {report['passed']} passed, "
248
+ f"{report['failed']} failed, {report['skipped']} skipped"
249
+ ]
250
+ for r in report["results"]:
251
+ if r["status"] != "passed":
252
+ detail = f": {r['detail']}" if r.get("detail") else ""
253
+ lines.append(f" [{r['status']}] {r['suite']}/{r['id']}{detail}")
254
+ lines.append("RESULT: conformant" if report["conformant"] else "RESULT: not conformant")
255
+ return "\n".join(lines)
256
+
257
+
258
+ def main(argv: list[str] | None = None) -> int:
259
+ args = list(sys.argv[1:] if argv is None else argv)
260
+ vectors = load_vectors(args[0]) if args else load_vectors()
261
+ report = run_conformance(vectors)
262
+ print(format_report(report))
263
+ return 0 if report["conformant"] else 1
264
+
265
+
266
+ if __name__ == "__main__":
267
+ raise SystemExit(main())