Files
msime-windows/scripts/dictionary_product.py
fanlusky a7779d2b05 refactor(engine): 将 engine 从 submodule 内嵌为仓内一等组件
`vendor/MetasequoiaImeEngine` submodule 改为 `engine/` 目录,导入的是
product-lock.json 原本钉住的提交 c810d201,逐文件核对过 blob,因此这是一次
搬迁而不是引擎升级。引擎自己的嵌套 submodule(googlepinyinime-rev、utfcpp、
miniaudio)按各自 gitlink 一并展开,来源与提交见 engine/UPSTREAM.md。

Windows 专门化裁掉了不属于本产品的部分:whisper.cpp 与本地推理 provider、
macOS 示例宿主、整个 dictionary/ 建库流水线、上游 CI 与 release-please 配置。
词库仍按 product-lock.json 在构建时取发布产物。

引擎不再进 product-lock.json:没有 gitlink 就没有可比对的东西,锁里写一个上游
commit 等于宣称这棵树仍等于上游,而第一个 Windows 专门化改动落地时这句话就假了。
verify-contracts / verify-checkout 随之移除,verify-published 保留。

CI 与工具链:引擎源码并入根 scripts/format.sh(删掉引擎自己的那份,避免两套排除
列表分叉);引擎 voice.yml 里的 silero_vad.onnx 摘要校验移到本仓 lock job;
engine-bump-triage 工作流与其测试删除;gitsubmodule 的 dependabot 频率由 daily
改为 monthly。

README、README.en、AGENTS.md、CONTRIBUTING、docs/product-release.md 及各子目录
文档同步说明新的位置与边界。
2026-09-12 03:40:48 +08:00

60 lines
2.9 KiB
Python

"""Public build-time dictionary product validation (Python standard library only).
Consumers vendor this file from the engine's contract directory. CI must compare
the vendored bytes with this copy. Lock-file digests remain the trust anchor; this
validator additionally checks format/profile compatibility and selected payloads.
"""
import hashlib
import json
from pathlib import Path
import re
MANIFEST_NAME = 'dictionary-manifest.json'
SUPPORTED_FORMATS = (1,)
DESKTOP_FILES = frozenset({'msime.db', 'english.db', 'others.db', 'dict_japanese.dat', 'mozc_dictionary_oss_README.txt'})
MOBILE_FILES = frozenset({'msime.db', 'msime.db.sha256'})
def sha256(path):
digest = hashlib.sha256()
with Path(path).open('rb') as stream:
for chunk in iter(lambda: stream.read(1 << 20), b''):
digest.update(chunk)
return digest.hexdigest()
def verify_product(directory, profile='desktop', required_files=None):
directory = Path(directory)
manifest = json.loads((directory / MANIFEST_NAME).read_text(encoding='utf-8'))
version = manifest.get('format_version')
if (manifest.get('manifest_version') != 1 or type(version) is not int or
version not in SUPPORTED_FORMATS or
manifest.get('engine_compatibility', {}).get('dictionary_format') != version):
raise ValueError('Unsupported or inconsistent dictionary product format')
if profile not in ('desktop', 'mobile') or manifest.get('profile') != profile:
raise ValueError('Dictionary product profile mismatch')
expected = DESKTOP_FILES if profile == 'desktop' else MOBILE_FILES
files = manifest.get('files', {})
if set(files) != expected:
raise ValueError('Dictionary artifact set does not match its profile')
selected = expected if required_files is None else frozenset(required_files)
if not selected <= expected:
raise ValueError('Requested files are not part of this dictionary product')
for name, entry in files.items():
if (Path(name).name != name or '/' in name or '\\' in name or
not re.fullmatch('[0-9a-f]{64}', entry.get('sha256', '')) or
type(entry.get('size')) is not int or entry['size'] < 0):
raise ValueError(f'Invalid dictionary artifact metadata: {name}')
if name in selected:
path = directory / name
if not path.is_file() or path.stat().st_size != entry['size'] or sha256(path) != entry['sha256']:
raise ValueError(f'Dictionary artifact changed: {name}')
if profile == 'desktop':
if manifest.get('engine_compatibility', {}).get('japanese_model_magic') != 'MSJPDT1':
raise ValueError('Unsupported Japanese model format')
if 'dict_japanese.dat' in selected:
with (directory / 'dict_japanese.dat').open('rb') as model:
if model.read(8) != b'MSJPDT1\0':
raise ValueError('Japanese model header does not match its declared format')
return manifest