Files
msime-windows/scripts/dictionary_product.py
T

60 lines
2.9 KiB
Python

"""Public build-time dictionary product validation (Python standard library only).
Consumers may vendor this file from their pinned Engine contract. CI must compare
vendored bytes with that gitlink. 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