Files
curios/tools/invalid-fixtures.py
T

105 lines
5.8 KiB
Python

"""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', '<!doctype html><title>Invalid fixture</title>')
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}')