Files
main_plugin_farm/src/farm_render/font_bundle.py
T
2026-10-26 11:34:00 +08:00

107 lines
3.8 KiB
Python

"""从 service 仓库下载原版小赖字体,校验后原子写入本地缓存。"""
import hashlib
from importlib import resources
import json
from pathlib import Path
import tempfile
from zipfile import ZipFile
from .bundle import _LOCK, _copy_remote, validate_options
FONT_NAME = "Xiaolai-Regular.ttf"
FILES = {
FONT_NAME: (
22220806,
"e2f68daf0e72777a8cf58bc83de1b98634b251e537ddbfca24b0ae50d1802da2",
),
"OFL-Xiaolai.txt": (
4432,
"0df7e09be4c2c850a48bd8beb9cd64b343aad49cd5d3f6cfb2ad2e3d28a56ca4",
),
}
def expected_manifest():
return json.loads(resources.files(__package__).joinpath(
"data", "font-manifest.json",
).read_bytes())
def _valid(path, size, digest):
if not path.is_file() or path.stat().st_size != size:
return False
checksum = hashlib.sha256()
with path.open("rb") as stream:
for chunk in iter(lambda: stream.read(65536), b""):
checksum.update(chunk)
return checksum.hexdigest() == digest
def _atomic_write(path, content):
temporary = None
try:
with tempfile.NamedTemporaryFile(
dir=path.parent, suffix=".download", delete=False,
) as stream:
temporary = Path(stream.name)
stream.write(content)
temporary.replace(path)
finally:
if temporary is not None:
temporary.unlink(missing_ok=True)
def ensure_font(data_dir, source="Github", service_url=""):
"""校验全部缓存文件;无效时下载,始终同时保留完整 OFL 许可。"""
validate_options(source, service_url)
manifest = expected_manifest()
root = Path(data_dir) / "fonts" / manifest["sha256"]
with _LOCK:
if all(_valid(root / name, *checks)
for name, checks in FILES.items()):
return root / FONT_NAME
root.mkdir(parents=True, exist_ok=True)
archive_path = root / "assets.zip"
if not _valid(archive_path, manifest["size_bytes"],
manifest["sha256"]):
url = (
service_url.rstrip("/") + "/v1/fonts/"
+ manifest["sha256"] + "/assets.zip"
if source == "farm_service"
else manifest["sources"][source.lower()]
)
temporary = None
try:
with tempfile.NamedTemporaryFile(
dir=root, suffix=".download", delete=False,
) as stream:
temporary = Path(stream.name)
_copy_remote(url, stream, manifest["size_bytes"])
if not _valid(temporary, manifest["size_bytes"],
manifest["sha256"]):
raise ValueError("Font archive size or SHA256 mismatch")
temporary.replace(archive_path)
finally:
if temporary is not None:
temporary.unlink(missing_ok=True)
with ZipFile(archive_path) as archive:
entries = archive.infolist()
if (len(entries) != len(FILES)
or {item.filename for item in entries} != set(FILES)):
raise ValueError("Unexpected font archive contents")
content = {}
for item in entries:
size, digest = FILES[item.filename]
if item.file_size != size or item.flag_bits & 1:
raise ValueError("Invalid font archive member")
value = archive.read(item)
if hashlib.sha256(value).hexdigest() != digest:
raise ValueError("Font member SHA256 mismatch")
content[item.filename] = value
# 先写许可,再使字体可用;不使用 extractall 或远端提供的路径。
for name in ("OFL-Xiaolai.txt", FONT_NAME):
_atomic_write(root / name, content[name])
return root / FONT_NAME