Codex commited on
Commit
c5eaa88
·
1 Parent(s): b69b686

Harden glyph planner manifest and marker preflight

Browse files
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 it contains ``@`` or begins exactly with ``http://`` or ``https://``.
10
- Every such run must independently satisfy the strict ``glyph_ascii_v1``
11
- grammar. There is no stripping, case folding, Unicode repair, substring
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
- _NETWORK_PREFIXES = ("http://", "https://")
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
- return "@" in run or run.startswith(_NETWORK_PREFIXES)
 
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
- if _network_looking(match.group(0)):
239
- network_runs.append(match)
 
 
 
 
 
 
 
 
 
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 _manifest_digest(
296
  asset_entry_sha256_by_asset_id: Mapping[str, str],
297
- ) -> str:
 
 
298
  try:
299
- return glyph_ascii_v1_asset_manifest_sha256(
300
- asset_entry_sha256_by_asset_id
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 = _manifest_digest(asset_entry_sha256_by_asset_id)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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=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 = _manifest_digest(asset_entry_sha256_by_asset_id)
 
 
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=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 = build_glyph_hybrid_plan(
784
  reconstructed,
785
- asset_entry_sha256_by_asset_id=asset_entry_sha256_by_asset_id,
 
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