"""Generate deliberately invalid .curio archives. Never publish these in the normal source.""" import copy import json from pathlib import Path import stat import zipfile ROOT = Path(__file__).resolve().parent.parent OUT = ROOT / 'dist' / 'invalid' OUT.mkdir(parents=True, exist_ok=True) BASE = { 'schemaVersion': 1, 'id': 'dev.trove.qa.invalid', 'name': '故意无效的测试包', 'version': '1.0.0', 'engine': {'trove': '>=0.1.0 <1.0.0'}, 'ui': {'entry': 'ui/index.html'}, } cases = [] def archive(name, code, mutate=None, entries=None, raw=None): manifest = copy.deepcopy(BASE) if mutate: mutate(manifest) file = OUT / f'{name}.curio' with zipfile.ZipFile(file, 'w', zipfile.ZIP_DEFLATED) as bundle: bundle.writestr('curio.json', raw if raw is not None else json.dumps(manifest)) bundle.writestr('ui/index.html', 'Invalid fixture') for entry, content in entries or []: bundle.writestr(entry, content) cases.append({'name': name, 'file': str(file), 'expectedError': code}) archive('malformed-manifest', 'package.invalid_manifest', raw='{broken') archive('unknown-manifest-field', 'package.invalid_manifest', lambda m: m.update(unknown=True)) archive('unsupported-schema', 'package.invalid_manifest', lambda m: m.update(schemaVersion=2)) archive('reserved-id', 'package.invalid_id', lambda m: m.update(id='host.qa.invalid')) archive('invalid-version', 'package.invalid_manifest', lambda m: m.update(version='not-semver')) archive('incompatible-engine', 'package.engine_incompatible', lambda m: m.update(engine={'trove': '>=99.0.0'})) archive('missing-ui', 'package.missing_entry', lambda m: m['ui'].update(entry='ui/missing.html')) archive('no-ui-or-backend', 'package.invalid_manifest', lambda m: m.pop('ui')) archive('manifest-path-traversal', 'package.invalid_manifest', lambda m: m['ui'].update(entry='../escape.html')) archive('reserved-backend-env', 'package.invalid_manifest', lambda m: m.update(backend={ 'entry': 'backend/probe', 'lifecycle': 'on-demand', 'transport': 'unix', 'env': {'TROVE_SESSION_TOKEN': 'override'} })) archive('service-without-backend', 'package.invalid_manifest', lambda m: m.update(services={'provides': [ {'id': m['id'] + '.probe', 'version': '1.0.0', 'contract': 'contracts/probe.json'} ]})) def provider(m): m['backend'] = {'entry': 'backend/probe', 'lifecycle': 'on-demand', 'transport': 'unix'} m['services'] = {'provides': [{'id': m['id'] + '.probe', 'version': '1.0.0', 'contract': 'contracts/probe.json'}]} def wrong_namespace(m): provider(m) m['services']['provides'][0]['id'] = 'dev.someone.else.probe' archive('service-namespace', 'package.invalid_manifest', wrong_namespace) archive('missing-backend', 'package.missing_entry', provider) archive('missing-contract', 'package.missing_entry', provider, [('backend/probe', 'placeholder')]) contract = {'schemaVersion': 1, 'service': BASE['id'] + '.probe', 'version': '2.0.0', 'methods': {}} archive('contract-version-mismatch', 'package.contract_mismatch', provider, [('backend/probe', 'placeholder'), ('contracts/probe.json', json.dumps(contract))]) contract['version'] = '1.0.0' contract['methods'] = {'probe': {'params': {'type': 'not-a-json-type'}, 'result': {}}} archive('invalid-contract-schema', 'package.invalid_contract', provider, [('backend/probe', 'placeholder'), ('contracts/probe.json', json.dumps(contract))]) archive('zip-path-traversal', 'package.unsafe_path', entries=[('../qa-escape.txt', 'must not escape')]) archive('zip-absolute-path', 'package.unsafe_path', entries=[('/tmp/trove-qa-escape.txt', 'must not escape')]) archive('zip-backslash-path', 'package.unsafe_path', entries=[('ui\\escape.txt', 'must not be accepted')]) archive('zip-case-collision', 'package.unsafe_archive', entries=[('UI/INDEX.HTML', 'collides on macOS')]) symlink = zipfile.ZipInfo('ui/link') symlink.create_system = 3 symlink.external_attr = (stat.S_IFLNK | 0o777) << 16 archive('zip-symlink', 'package.unsafe_archive', entries=[(symlink, '../../outside')]) truncated = OUT / 'truncated-archive.curio' truncated.write_bytes(b'PK\x03\x04broken') cases.append({'name': 'truncated-archive', 'file': str(truncated), 'expectedError': 'package.invalid_archive'}) (OUT / 'cases.json').write_text(json.dumps(cases, ensure_ascii=False, indent=2) + '\n') # Valid source schemas carrying wrong download metadata test checks after index parsing. baseline = json.loads((ROOT / 'dist/index.json').read_text()) baseline['curios'] = [next(item for item in baseline['curios'] if item['id'] == 'dev.trove.qa.host')] source_cases = [] for name, field, value, expected in [ ('hash-mismatch', 'sha256', '0' * 64, 'package.hash_mismatch'), ('size-mismatch', 'size', 1, 'package.size_mismatch'), ('wrong-target', 'target', 'x86_64-apple-darwin' if __import__('platform').machine() == 'arm64' else 'aarch64-apple-darwin', 'package.target_mismatch'), ]: index = copy.deepcopy(baseline) index['id'] = 'qa-' + name index['curios'][0]['versions'][0]['artifacts'][0][field] = value file = OUT / f'{name}-index.json' file.write_text(json.dumps(index, ensure_ascii=False, indent=2) + '\n') source_cases.append({'name': name, 'id': index['id'], 'file': str(file), 'expectedError': expected}) index = copy.deepcopy(baseline) index['id'] = 'qa-identity-mismatch' index['curios'][0]['id'] = 'dev.trove.qa.wrong-identity' file = OUT / 'identity-mismatch-index.json' file.write_text(json.dumps(index, ensure_ascii=False, indent=2) + '\n') source_cases.append({'name': 'identity-mismatch', 'id': index['id'], 'file': str(file), 'expectedError': 'package.identity_mismatch', 'curioId': 'dev.trove.qa.wrong-identity'}) (OUT / 'source-cases.json').write_text(json.dumps(source_cases, ensure_ascii=False, indent=2) + '\n') print(f'Generated {len(cases)} invalid archives and {len(source_cases)} adverse sources in {OUT}')