返回 ppt-master
fallback_hash.py
1 """Visible-fallback fingerprint contract for native chart/table markers."""
2
3 from __future__ import annotations
4
5 import re
6 import secrets
7 from xml.etree import ElementTree as ET
8
9 from pptx_shapes import (
10 NATIVE_FALLBACK_SHA256_ATTR,
11 svg_native_fallback_fingerprint,
12 )
13
14 from .marker_attributes import native_import_source, native_replacement_kind
15
16
17 NATIVE_FALLBACK_RUNTIME_ATTR = "data-pptx-runtime-fallback-unchanged"
18 _NATIVE_FALLBACK_RUNTIME_TOKEN_ATTR = "data-pptx-runtime-fallback-token"
19 _SHA256_RE = re.compile(r"^[0-9a-fA-F]{64}$")
20 _RUNTIME_TOKEN = secrets.token_hex(16)
21
22
23 def stamp_native_fallback_baseline(
24 elem: ET.Element,
25 *,
26 document_root: ET.Element | None = None,
27 ) -> str:
28 """Record the current canonical visible-subtree hash on one marker."""
29 digest = svg_native_fallback_fingerprint(
30 elem,
31 document_root=document_root,
32 )
33 elem.set(NATIVE_FALLBACK_SHA256_ATTR, digest)
34 elem.attrib.pop(NATIVE_FALLBACK_RUNTIME_ATTR, None)
35 elem.attrib.pop(_NATIVE_FALLBACK_RUNTIME_TOKEN_ATTR, None)
36 return digest
37
38
39 def snapshot_native_fallback_freshness(root: ET.Element) -> None:
40 """Snapshot raw marker freshness before exporter preprocessing mutates SVG."""
41 for elem in root.iter():
42 if elem.tag.rsplit("}", 1)[-1] == "metadata":
43 continue
44 if not native_replacement_kind(elem):
45 continue
46 expected, invalid = _expected_native_fallback_hash(elem)
47 if invalid:
48 elem.set(NATIVE_FALLBACK_RUNTIME_ATTR, "invalid")
49 elem.set(_NATIVE_FALLBACK_RUNTIME_TOKEN_ATTR, _RUNTIME_TOKEN)
50 elif expected is None:
51 elem.attrib.pop(NATIVE_FALLBACK_RUNTIME_ATTR, None)
52 elem.attrib.pop(_NATIVE_FALLBACK_RUNTIME_TOKEN_ATTR, None)
53 else:
54 actual = svg_native_fallback_fingerprint(
55 elem,
56 document_root=root,
57 )
58 elem.set(
59 NATIVE_FALLBACK_RUNTIME_ATTR,
60 "1" if actual == expected else "0",
61 )
62 elem.set(_NATIVE_FALLBACK_RUNTIME_TOKEN_ATTR, _RUNTIME_TOKEN)
63
64
65 def native_fallback_contract_warnings(
66 elem: ET.Element,
67 *,
68 use_runtime_snapshot: bool = False,
69 document_root: ET.Element | None = None,
70 ) -> list[str]:
71 """Return non-blocking diagnostics for default/checker compatibility."""
72 expected, invalid = _expected_native_fallback_hash(elem)
73 if invalid:
74 return [
75 f"{NATIVE_FALLBACK_SHA256_ATTR} must be a 64-digit SHA-256; "
76 "the shape-based SVG fallback remains available, but "
77 "--native-charts-and-tables will fail"
78 ]
79 if expected is None:
80 if native_import_source(elem) != "pptx":
81 return []
82 return [
83 f"imported marker has no {NATIVE_FALLBACK_SHA256_ATTR} baseline; "
84 "the marker remains compatible with Chart/Table replacement, but "
85 "stale fallback edits cannot be detected"
86 ]
87 if _native_fallback_is_fresh(
88 elem,
89 expected,
90 use_runtime_snapshot=use_runtime_snapshot,
91 document_root=document_root,
92 ):
93 return []
94 return [
95 "visible SVG fallback differs from its recorded baseline; default "
96 "shape-based fallback export remains available, but "
97 "--native-charts-and-tables will fail"
98 ]
99
100
101 def require_fresh_native_fallback(
102 elem: ET.Element,
103 *,
104 use_runtime_snapshot: bool = False,
105 document_root: ET.Element | None = None,
106 ) -> None:
107 """Fail Chart/Table replacement when a recorded fallback is stale."""
108 expected, invalid = _expected_native_fallback_hash(elem)
109 if invalid:
110 raise RuntimeError(
111 f"{NATIVE_FALLBACK_SHA256_ATTR} must be a 64-digit SHA-256"
112 )
113 if expected is None:
114 return
115 if _native_fallback_is_fresh(
116 elem,
117 expected,
118 use_runtime_snapshot=use_runtime_snapshot,
119 document_root=document_root,
120 ):
121 return
122 raise RuntimeError(
123 "Visible native-object SVG fallback was edited after its baseline was "
124 "recorded; --native-charts-and-tables stopped to avoid discarding the "
125 "SVG edit. "
126 "Use the default fallback export, or deliberately update the native "
127 "metadata and baseline together"
128 )
129
130
131 def _expected_native_fallback_hash(
132 elem: ET.Element,
133 ) -> tuple[str | None, bool]:
134 raw = elem.get(NATIVE_FALLBACK_SHA256_ATTR)
135 if raw is None:
136 return None, False
137 if raw != raw.strip() or _SHA256_RE.fullmatch(raw) is None:
138 return None, True
139 return raw.lower(), False
140
141
142 def _native_fallback_is_fresh(
143 elem: ET.Element,
144 expected: str,
145 *,
146 use_runtime_snapshot: bool,
147 document_root: ET.Element | None,
148 ) -> bool:
149 if (
150 use_runtime_snapshot
151 and elem.get(_NATIVE_FALLBACK_RUNTIME_TOKEN_ATTR) == _RUNTIME_TOKEN
152 ):
153 snapshot = elem.get(NATIVE_FALLBACK_RUNTIME_ATTR)
154 if snapshot == "1":
155 return True
156 if snapshot in {"0", "invalid"}:
157 return False
158 return svg_native_fallback_fingerprint(
159 elem,
160 document_root=document_root,
161 ) == expected
162
162 lines PYTHON