Spaces:
Running on Zero
Running on Zero
Codex commited on
Commit ·
c5eaa88
1
Parent(s): b69b686
Harden glyph planner manifest and marker preflight
Browse files- glyph_hybrid_plan.py +76 -19
- tests/test_glyph_hybrid_plan.py +113 -0
glyph_hybrid_plan.py
CHANGED
|
@@ -6,10 +6,10 @@ request either receives one complete, reversible hybrid plan or remains on the
|
|
| 6 |
unchanged legacy path.
|
| 7 |
|
| 8 |
The profile scans *maximal printable-ASCII runs*. A run is network-looking
|
| 9 |
-
when
|
| 10 |
-
Every such run must independently satisfy the strict
|
| 11 |
-
grammar. There is no stripping, case folding,
|
| 12 |
-
borrowing, or partial activation.
|
| 13 |
|
| 14 |
Canonical boundary contract
|
| 15 |
---------------------------
|
|
@@ -28,6 +28,7 @@ from dataclasses import asdict, dataclass
|
|
| 28 |
import hashlib
|
| 29 |
import json
|
| 30 |
import re
|
|
|
|
| 31 |
import unicodedata
|
| 32 |
|
| 33 |
from glyph_ascii_v1 import (
|
|
@@ -35,6 +36,7 @@ from glyph_ascii_v1 import (
|
|
| 35 |
GlyphAsciiV1Error,
|
| 36 |
GlyphAsciiV1Proof,
|
| 37 |
glyph_ascii_v1_asset_manifest_sha256,
|
|
|
|
| 38 |
inverse_glyph_ascii_v1,
|
| 39 |
render_glyph_ascii_v1,
|
| 40 |
)
|
|
@@ -66,7 +68,7 @@ GLYPH_HYBRID_MAX_MODEL_GENERATED_TEXT_UNITS = 800
|
|
| 66 |
|
| 67 |
_PRINTABLE_ASCII_RUN_RE = re.compile(r"[ -~]+", flags=re.ASCII)
|
| 68 |
_SHA256_RE = re.compile(r"[0-9a-f]{64}\Z", flags=re.ASCII)
|
| 69 |
-
|
| 70 |
_ZH_CARRIER_COMPATIBILITY_PUNCTUATION = frozenset(
|
| 71 |
",。!?;:、()【】「」『』《》〈〉“”‘’[]{}"
|
| 72 |
"…⋯—–.。、〔〕〖〗〘〙〚〛﹁﹂︰﹐﹑﹒﹔﹕﹖﹗"
|
|
@@ -193,7 +195,8 @@ def glyph_hybrid_plan_sha256(plan: HybridRenderPlan) -> str:
|
|
| 193 |
|
| 194 |
|
| 195 |
def _network_looking(run: str) -> bool:
|
| 196 |
-
|
|
|
|
| 197 |
|
| 198 |
|
| 199 |
def _request_has_unsafe_unicode(raw_request: str) -> bool:
|
|
@@ -235,8 +238,17 @@ def _request_has_unsafe_unicode(raw_request: str) -> bool:
|
|
| 235 |
def _raw_pieces(raw_request: str) -> tuple[_RawPiece, ...] | None:
|
| 236 |
network_runs: list[re.Match[str]] = []
|
| 237 |
for match in _PRINTABLE_ASCII_RUN_RE.finditer(raw_request):
|
| 238 |
-
|
| 239 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 240 |
if not network_runs:
|
| 241 |
return None
|
| 242 |
if len(network_runs) > GLYPH_HYBRID_MAX_IDENTIFIERS:
|
|
@@ -292,15 +304,26 @@ def _carrier_canonical(raw_fragment: str) -> str | None:
|
|
| 292 |
return canonical
|
| 293 |
|
| 294 |
|
| 295 |
-
def
|
| 296 |
asset_entry_sha256_by_asset_id: Mapping[str, str],
|
| 297 |
-
) -> str:
|
|
|
|
|
|
|
| 298 |
try:
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 302 |
except GlyphAsciiV1Error as error:
|
| 303 |
raise GlyphHybridPlanError("glyph asset manifest is invalid") from error
|
|
|
|
| 304 |
|
| 305 |
|
| 306 |
def _segment(
|
|
@@ -351,7 +374,24 @@ def build_glyph_hybrid_plan(
|
|
| 351 |
|
| 352 |
if type(raw_request) is not str:
|
| 353 |
raise GlyphHybridPlanError("raw request must be an exact string")
|
| 354 |
-
manifest_sha256 =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 355 |
if (
|
| 356 |
not raw_request
|
| 357 |
or len(raw_request) > GLYPH_HYBRID_MAX_RAW_CHARS
|
|
@@ -430,12 +470,16 @@ def build_glyph_hybrid_plan(
|
|
| 430 |
append_separator(piece.start)
|
| 431 |
canonical, proof = render_glyph_ascii_v1(
|
| 432 |
piece.text,
|
| 433 |
-
asset_entry_sha256_by_asset_id=
|
| 434 |
absolute_raw_start=piece.start,
|
| 435 |
canonical_start=canonical_cursor,
|
| 436 |
)
|
| 437 |
except GlyphAsciiV1Error:
|
| 438 |
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
| 439 |
|
| 440 |
identifier_ordinal = len(identifier_proofs)
|
| 441 |
identifier_proofs.append(proof)
|
|
@@ -548,7 +592,9 @@ def inverse_glyph_hybrid_plan(
|
|
| 548 |
raise GlyphHybridPlanError("hybrid plan has an invalid type")
|
| 549 |
if type(expected_raw_request) is not str:
|
| 550 |
raise GlyphHybridPlanError("expected raw request must be an exact string")
|
| 551 |
-
manifest_sha256 =
|
|
|
|
|
|
|
| 552 |
if (
|
| 553 |
type(plan.identifier_proofs) is not tuple
|
| 554 |
or type(plan.segments) is not tuple
|
|
@@ -738,10 +784,20 @@ def inverse_glyph_hybrid_plan(
|
|
| 738 |
for identifier_ordinal, proof in enumerate(plan.identifier_proofs):
|
| 739 |
if type(proof) is not GlyphAsciiV1Proof:
|
| 740 |
raise GlyphHybridPlanError("identifier proof has an invalid type")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 741 |
try:
|
| 742 |
raw_identifier = inverse_glyph_ascii_v1(
|
| 743 |
proof,
|
| 744 |
-
asset_entry_sha256_by_asset_id=
|
| 745 |
)
|
| 746 |
except GlyphAsciiV1Error as error:
|
| 747 |
raise GlyphHybridPlanError("identifier proof is invalid") from error
|
|
@@ -780,9 +836,10 @@ def inverse_glyph_hybrid_plan(
|
|
| 780 |
if set(asset_segments) != expected_asset_keys:
|
| 781 |
raise GlyphHybridPlanError("asset segment set does not match identifier proofs")
|
| 782 |
|
| 783 |
-
expected_plan =
|
| 784 |
reconstructed,
|
| 785 |
-
|
|
|
|
| 786 |
)
|
| 787 |
if (
|
| 788 |
expected_plan is None
|
|
|
|
| 6 |
unchanged legacy path.
|
| 7 |
|
| 8 |
The profile scans *maximal printable-ASCII runs*. A run is network-looking
|
| 9 |
+
when a case-insensitive preflight finds a network marker anywhere within it.
|
| 10 |
+
Every such complete run must independently satisfy the strict, native-
|
| 11 |
+
lowercase ``glyph_ascii_v1`` grammar. There is no stripping, case folding,
|
| 12 |
+
Unicode repair, substring borrowing, or partial activation.
|
| 13 |
|
| 14 |
Canonical boundary contract
|
| 15 |
---------------------------
|
|
|
|
| 28 |
import hashlib
|
| 29 |
import json
|
| 30 |
import re
|
| 31 |
+
from types import MappingProxyType
|
| 32 |
import unicodedata
|
| 33 |
|
| 34 |
from glyph_ascii_v1 import (
|
|
|
|
| 36 |
GlyphAsciiV1Error,
|
| 37 |
GlyphAsciiV1Proof,
|
| 38 |
glyph_ascii_v1_asset_manifest_sha256,
|
| 39 |
+
glyph_ascii_v1_identifier_kind,
|
| 40 |
inverse_glyph_ascii_v1,
|
| 41 |
render_glyph_ascii_v1,
|
| 42 |
)
|
|
|
|
| 68 |
|
| 69 |
_PRINTABLE_ASCII_RUN_RE = re.compile(r"[ -~]+", flags=re.ASCII)
|
| 70 |
_SHA256_RE = re.compile(r"[0-9a-f]{64}\Z", flags=re.ASCII)
|
| 71 |
+
_NETWORK_MARKERS = ("http://", "https://", "ftp://", "www.", "@", "://")
|
| 72 |
_ZH_CARRIER_COMPATIBILITY_PUNCTUATION = frozenset(
|
| 73 |
",。!?;:、()【】「」『』《》〈〉“”‘’[]{}"
|
| 74 |
"…⋯—–.。、〔〕〖〗〘〙〚〛﹁﹂︰﹐﹑﹒﹔﹕﹖﹗"
|
|
|
|
| 195 |
|
| 196 |
|
| 197 |
def _network_looking(run: str) -> bool:
|
| 198 |
+
folded = run.lower()
|
| 199 |
+
return any(marker in folded for marker in _NETWORK_MARKERS)
|
| 200 |
|
| 201 |
|
| 202 |
def _request_has_unsafe_unicode(raw_request: str) -> bool:
|
|
|
|
| 238 |
def _raw_pieces(raw_request: str) -> tuple[_RawPiece, ...] | None:
|
| 239 |
network_runs: list[re.Match[str]] = []
|
| 240 |
for match in _PRINTABLE_ASCII_RUN_RE.finditer(raw_request):
|
| 241 |
+
run = match.group(0)
|
| 242 |
+
if not _network_looking(run):
|
| 243 |
+
continue
|
| 244 |
+
try:
|
| 245 |
+
# A marker anywhere in a maximal printable-ASCII run makes that
|
| 246 |
+
# complete run security-relevant. Only the exact, lowercase v1
|
| 247 |
+
# grammar may activate; never borrow a valid-looking substring.
|
| 248 |
+
glyph_ascii_v1_identifier_kind(run)
|
| 249 |
+
except GlyphAsciiV1Error:
|
| 250 |
+
return None
|
| 251 |
+
network_runs.append(match)
|
| 252 |
if not network_runs:
|
| 253 |
return None
|
| 254 |
if len(network_runs) > GLYPH_HYBRID_MAX_IDENTIFIERS:
|
|
|
|
| 304 |
return canonical
|
| 305 |
|
| 306 |
|
| 307 |
+
def _manifest_snapshot(
|
| 308 |
asset_entry_sha256_by_asset_id: Mapping[str, str],
|
| 309 |
+
) -> tuple[Mapping[str, str], str]:
|
| 310 |
+
if not isinstance(asset_entry_sha256_by_asset_id, Mapping):
|
| 311 |
+
raise GlyphHybridPlanError("glyph asset manifest entries must be a mapping")
|
| 312 |
try:
|
| 313 |
+
# A caller-supplied Mapping may compute or cycle values on each
|
| 314 |
+
# access. Materialize it exactly once into an owned plain dict, then
|
| 315 |
+
# expose only an immutable view for every subsequent proof operation.
|
| 316 |
+
owned = dict(asset_entry_sha256_by_asset_id.items())
|
| 317 |
+
except Exception as error:
|
| 318 |
+
raise GlyphHybridPlanError(
|
| 319 |
+
"glyph asset manifest cannot be materialized"
|
| 320 |
+
) from error
|
| 321 |
+
snapshot: Mapping[str, str] = MappingProxyType(owned)
|
| 322 |
+
try:
|
| 323 |
+
digest = glyph_ascii_v1_asset_manifest_sha256(snapshot)
|
| 324 |
except GlyphAsciiV1Error as error:
|
| 325 |
raise GlyphHybridPlanError("glyph asset manifest is invalid") from error
|
| 326 |
+
return snapshot, digest
|
| 327 |
|
| 328 |
|
| 329 |
def _segment(
|
|
|
|
| 374 |
|
| 375 |
if type(raw_request) is not str:
|
| 376 |
raise GlyphHybridPlanError("raw request must be an exact string")
|
| 377 |
+
manifest_snapshot, manifest_sha256 = _manifest_snapshot(
|
| 378 |
+
asset_entry_sha256_by_asset_id
|
| 379 |
+
)
|
| 380 |
+
return _build_glyph_hybrid_plan_from_snapshot(
|
| 381 |
+
raw_request,
|
| 382 |
+
manifest_snapshot=manifest_snapshot,
|
| 383 |
+
manifest_sha256=manifest_sha256,
|
| 384 |
+
)
|
| 385 |
+
|
| 386 |
+
|
| 387 |
+
def _build_glyph_hybrid_plan_from_snapshot(
|
| 388 |
+
raw_request: str,
|
| 389 |
+
*,
|
| 390 |
+
manifest_snapshot: Mapping[str, str],
|
| 391 |
+
manifest_sha256: str,
|
| 392 |
+
) -> HybridRenderPlan | None:
|
| 393 |
+
"""Build using one already validated, immutable manifest snapshot."""
|
| 394 |
+
|
| 395 |
if (
|
| 396 |
not raw_request
|
| 397 |
or len(raw_request) > GLYPH_HYBRID_MAX_RAW_CHARS
|
|
|
|
| 470 |
append_separator(piece.start)
|
| 471 |
canonical, proof = render_glyph_ascii_v1(
|
| 472 |
piece.text,
|
| 473 |
+
asset_entry_sha256_by_asset_id=manifest_snapshot,
|
| 474 |
absolute_raw_start=piece.start,
|
| 475 |
canonical_start=canonical_cursor,
|
| 476 |
)
|
| 477 |
except GlyphAsciiV1Error:
|
| 478 |
return None
|
| 479 |
+
if proof.asset_manifest_sha256 != manifest_sha256:
|
| 480 |
+
raise GlyphHybridPlanError(
|
| 481 |
+
"identifier proof manifest does not match plan header"
|
| 482 |
+
)
|
| 483 |
|
| 484 |
identifier_ordinal = len(identifier_proofs)
|
| 485 |
identifier_proofs.append(proof)
|
|
|
|
| 592 |
raise GlyphHybridPlanError("hybrid plan has an invalid type")
|
| 593 |
if type(expected_raw_request) is not str:
|
| 594 |
raise GlyphHybridPlanError("expected raw request must be an exact string")
|
| 595 |
+
manifest_snapshot, manifest_sha256 = _manifest_snapshot(
|
| 596 |
+
asset_entry_sha256_by_asset_id
|
| 597 |
+
)
|
| 598 |
if (
|
| 599 |
type(plan.identifier_proofs) is not tuple
|
| 600 |
or type(plan.segments) is not tuple
|
|
|
|
| 784 |
for identifier_ordinal, proof in enumerate(plan.identifier_proofs):
|
| 785 |
if type(proof) is not GlyphAsciiV1Proof:
|
| 786 |
raise GlyphHybridPlanError("identifier proof has an invalid type")
|
| 787 |
+
if (
|
| 788 |
+
_require_sha256(
|
| 789 |
+
proof.asset_manifest_sha256,
|
| 790 |
+
field="proof.asset_manifest_sha256",
|
| 791 |
+
)
|
| 792 |
+
!= plan.asset_manifest_logical_sha256
|
| 793 |
+
):
|
| 794 |
+
raise GlyphHybridPlanError(
|
| 795 |
+
"identifier proof manifest does not match plan header"
|
| 796 |
+
)
|
| 797 |
try:
|
| 798 |
raw_identifier = inverse_glyph_ascii_v1(
|
| 799 |
proof,
|
| 800 |
+
asset_entry_sha256_by_asset_id=manifest_snapshot,
|
| 801 |
)
|
| 802 |
except GlyphAsciiV1Error as error:
|
| 803 |
raise GlyphHybridPlanError("identifier proof is invalid") from error
|
|
|
|
| 836 |
if set(asset_segments) != expected_asset_keys:
|
| 837 |
raise GlyphHybridPlanError("asset segment set does not match identifier proofs")
|
| 838 |
|
| 839 |
+
expected_plan = _build_glyph_hybrid_plan_from_snapshot(
|
| 840 |
reconstructed,
|
| 841 |
+
manifest_snapshot=manifest_snapshot,
|
| 842 |
+
manifest_sha256=manifest_sha256,
|
| 843 |
)
|
| 844 |
if (
|
| 845 |
expected_plan is None
|
tests/test_glyph_hybrid_plan.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
|
|
|
| 3 |
from dataclasses import asdict, replace
|
| 4 |
import hashlib
|
| 5 |
import json
|
|
@@ -206,6 +207,21 @@ def test_carrier_uses_existing_zh_tw_normalization_without_network_content():
|
|
| 206 |
)
|
| 207 |
|
| 208 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 209 |
def _alpha(rng: random.Random, minimum: int = 1, maximum: int = 7) -> str:
|
| 210 |
return "".join(
|
| 211 |
rng.choice("abcdefghijklmnopqrstuvwxyz")
|
|
@@ -316,6 +332,27 @@ def test_one_invalid_network_run_rejects_the_entire_profile_without_partial_plan
|
|
| 316 |
assert _build(raw) is None
|
| 317 |
|
| 318 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 319 |
@pytest.mark.parametrize(
|
| 320 |
"raw",
|
| 321 |
(
|
|
@@ -560,6 +597,82 @@ def test_manifest_substitution_and_invalid_manifest_configuration_are_rejected()
|
|
| 560 |
)
|
| 561 |
|
| 562 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 563 |
def test_non_string_inputs_and_plan_subclasses_are_rejected():
|
| 564 |
class ForgedPlan(HybridRenderPlan):
|
| 565 |
pass
|
|
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
| 3 |
+
from collections.abc import Mapping
|
| 4 |
from dataclasses import asdict, replace
|
| 5 |
import hashlib
|
| 6 |
import json
|
|
|
|
| 207 |
)
|
| 208 |
|
| 209 |
|
| 210 |
+
def test_non_network_ascii_code_and_semver_remain_ordinary_carrier_content():
|
| 211 |
+
raw = "版本v1.2.3與程式C++保持原讀法,聯絡mead@forest.org。"
|
| 212 |
+
plan = _require_plan(raw)
|
| 213 |
+
carrier_raw = "".join(
|
| 214 |
+
segment.raw_text
|
| 215 |
+
for segment in plan.segments
|
| 216 |
+
if segment.kind in {"carrier", "control"}
|
| 217 |
+
)
|
| 218 |
+
|
| 219 |
+
assert "v1.2.3" in carrier_raw
|
| 220 |
+
assert "C++" in carrier_raw
|
| 221 |
+
assert len(plan.identifier_proofs) == 1
|
| 222 |
+
assert _inverse(plan, raw) == raw
|
| 223 |
+
|
| 224 |
+
|
| 225 |
def _alpha(rng: random.Random, minimum: int = 1, maximum: int = 7) -> str:
|
| 226 |
return "".join(
|
| 227 |
rng.choice("abcdefghijklmnopqrstuvwxyz")
|
|
|
|
| 332 |
assert _build(raw) is None
|
| 333 |
|
| 334 |
|
| 335 |
+
@pytest.mark.parametrize(
|
| 336 |
+
"malformed_run",
|
| 337 |
+
(
|
| 338 |
+
"foohttps://",
|
| 339 |
+
"9http://",
|
| 340 |
+
"fooHTTPS://",
|
| 341 |
+
"fooftp://",
|
| 342 |
+
"foowww.",
|
| 343 |
+
"foo://",
|
| 344 |
+
"@",
|
| 345 |
+
"https://",
|
| 346 |
+
),
|
| 347 |
+
)
|
| 348 |
+
def test_marker_anywhere_in_a_maximal_ascii_run_blocks_partial_activation(
|
| 349 |
+
malformed_run,
|
| 350 |
+
):
|
| 351 |
+
raw = f"聯絡mead@forest.org,異常值{malformed_run},結束。"
|
| 352 |
+
|
| 353 |
+
assert _build(raw) is None
|
| 354 |
+
|
| 355 |
+
|
| 356 |
@pytest.mark.parametrize(
|
| 357 |
"raw",
|
| 358 |
(
|
|
|
|
| 597 |
)
|
| 598 |
|
| 599 |
|
| 600 |
+
class _CyclingManifest(Mapping[str, str]):
|
| 601 |
+
"""Return the frozen value once, then a changed value on later reads."""
|
| 602 |
+
|
| 603 |
+
def __init__(self) -> None:
|
| 604 |
+
self.accesses = {asset_id: 0 for asset_id in ASSET_MANIFEST}
|
| 605 |
+
|
| 606 |
+
def __iter__(self):
|
| 607 |
+
return iter(ASSET_MANIFEST)
|
| 608 |
+
|
| 609 |
+
def __len__(self) -> int:
|
| 610 |
+
return len(ASSET_MANIFEST)
|
| 611 |
+
|
| 612 |
+
def __getitem__(self, asset_id: str) -> str:
|
| 613 |
+
count = self.accesses[asset_id]
|
| 614 |
+
self.accesses[asset_id] = count + 1
|
| 615 |
+
if count == 0:
|
| 616 |
+
return ASSET_MANIFEST[asset_id]
|
| 617 |
+
return hashlib.sha256(f"changed:{asset_id}".encode("ascii")).hexdigest()
|
| 618 |
+
|
| 619 |
+
|
| 620 |
+
def test_build_materializes_a_changing_manifest_exactly_once():
|
| 621 |
+
raw = (
|
| 622 |
+
"先寄mead@forest.org,再看https://audiocoast.org/tour,"
|
| 623 |
+
"確認完成。"
|
| 624 |
+
)
|
| 625 |
+
cycling = _CyclingManifest()
|
| 626 |
+
|
| 627 |
+
plan = build_glyph_hybrid_plan(
|
| 628 |
+
raw,
|
| 629 |
+
asset_entry_sha256_by_asset_id=cycling,
|
| 630 |
+
)
|
| 631 |
+
|
| 632 |
+
assert plan == _require_plan(raw)
|
| 633 |
+
assert set(cycling.accesses.values()) == {1}
|
| 634 |
+
assert all(
|
| 635 |
+
proof.asset_manifest_sha256 == plan.asset_manifest_logical_sha256
|
| 636 |
+
for proof in plan.identifier_proofs
|
| 637 |
+
)
|
| 638 |
+
|
| 639 |
+
|
| 640 |
+
def test_inverse_materializes_a_changing_manifest_exactly_once():
|
| 641 |
+
raw = (
|
| 642 |
+
"先寄mead@forest.org,再看https://audiocoast.org/tour,"
|
| 643 |
+
"確認完成。"
|
| 644 |
+
)
|
| 645 |
+
plan = _require_plan(raw)
|
| 646 |
+
cycling = _CyclingManifest()
|
| 647 |
+
|
| 648 |
+
reconstructed = inverse_glyph_hybrid_plan(
|
| 649 |
+
plan,
|
| 650 |
+
asset_entry_sha256_by_asset_id=cycling,
|
| 651 |
+
expected_raw_request=raw,
|
| 652 |
+
)
|
| 653 |
+
|
| 654 |
+
assert reconstructed == raw
|
| 655 |
+
assert set(cycling.accesses.values()) == {1}
|
| 656 |
+
|
| 657 |
+
|
| 658 |
+
def test_identifier_proof_manifest_digest_must_equal_plan_header():
|
| 659 |
+
raw = "請寄mead@forest.org。"
|
| 660 |
+
plan = _require_plan(raw)
|
| 661 |
+
proof = replace(plan.identifier_proofs[0], asset_manifest_sha256="0" * 64)
|
| 662 |
+
forged = replace(
|
| 663 |
+
plan,
|
| 664 |
+
identifier_proofs=(proof,),
|
| 665 |
+
plan_sha256="",
|
| 666 |
+
)
|
| 667 |
+
forged = replace(forged, plan_sha256=glyph_hybrid_plan_sha256(forged))
|
| 668 |
+
|
| 669 |
+
with pytest.raises(
|
| 670 |
+
GlyphHybridPlanError,
|
| 671 |
+
match="identifier proof manifest does not match plan header",
|
| 672 |
+
):
|
| 673 |
+
_inverse(forged, raw)
|
| 674 |
+
|
| 675 |
+
|
| 676 |
def test_non_string_inputs_and_plan_subclasses_are_rejected():
|
| 677 |
class ForgedPlan(HybridRenderPlan):
|
| 678 |
pass
|