`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 及各子目录 文档同步说明新的位置与边界。
60 lines
2.9 KiB
Python
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
|