Files
main_plugin_farm/scripts/verify_astrbot.py
T

88 lines
3.1 KiB
Python

"""AstrBot插件ZIP的独立完整性校验;只使用标准库。"""
import argparse
import hashlib
import json
from pathlib import Path, PurePosixPath
import stat
from zipfile import ZipFile
ROOT_NAME = 'astrbot_plugin_farm'
MANIFEST = 'FARM-MANIFEST.json'
MAX_BYTES = 100 * 1024 * 1024
def safe_path(name):
path = PurePosixPath(name)
if (not name or '\\' in name or ':' in name or path.is_absolute()
or any(p in ('', '.', '..') for p in name.split('/'))):
raise ValueError('Unsafe package path')
return path
def verify_files(files):
if MANIFEST not in files or len(files[MANIFEST]) > 1024 * 1024:
raise ValueError('Missing or oversized manifest')
manifest = json.loads(files[MANIFEST])
if (manifest.get('schema') != 1 or manifest.get('target') != 'astrbot'
or manifest.get('plugin') != ROOT_NAME):
raise ValueError('Invalid package identity')
expected = manifest.get('files')
if (not isinstance(expected, dict)
or set(files) != set(expected) | {MANIFEST}):
raise ValueError('Package files differ from manifest')
for name, digest in expected.items():
safe_path(name)
if hashlib.sha256(files[name]).hexdigest() != digest:
raise ValueError('Package checksum mismatch')
required = {'main.py', 'avatars.py', 'metadata.yaml', '_conf_schema.json',
'runtime.py',
'requirements.txt', 'LICENSE', 'version.txt', '_version.py',
'_vendor/farm_core/__init__.py',
'_vendor/farm_render/data/assets.zip'}
if not required <= set(files):
raise ValueError('Incomplete plugin')
metadata = json.loads(files['metadata.yaml'])
if (metadata.get('name') != ROOT_NAME
or metadata.get('version') != manifest['version']
or files['version.txt'].decode().strip() != manifest['version']):
raise ValueError('Package version mismatch')
return manifest
def read_package(path):
with ZipFile(path) as archive:
items = archive.infolist()
names = [item.filename for item in items]
if (len(names) != len(set(names)) or len(names) > 200
or sum(item.file_size for item in items) > MAX_BYTES):
raise ValueError('Duplicate paths or package too large')
files = {}
for item in items:
name = safe_path(item.filename)
if (len(name.parts) < 2 or name.parts[0] != ROOT_NAME
or item.is_dir()
or stat.S_ISLNK(item.external_attr >> 16)):
raise ValueError('Unexpected package root or link')
files[name.relative_to(ROOT_NAME).as_posix()] = archive.read(item)
verify_files(files)
return files
def verify_package(path):
return verify_files(read_package(path))
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('package', type=Path)
args = parser.parse_args()
manifest = verify_package(args.package)
print(f"Verified astrbot {manifest['version']}: "
f"{len(manifest['files'])} files")
if __name__ == '__main__':
main()