from __future__ import annotations import json import pathlib import generator.engraving.validation import generator.fretboard import generator.performance import generator.pico_artwork import generator.validation def load_json(path: pathlib.Path) -> dict: document = json.loads(path.read_text(encoding='utf-8')) return validate_json(document, path) def validate_json(document: dict, path: pathlib.Path, validated_pages: set[str] | None = None) -> dict: if not isinstance(document, dict): raise ValueError('expected a score object') if 'music' in document: presentation = document.get('presentation', {}) if not isinstance(document['music'], dict) or not isinstance(presentation, dict) or not isinstance(presentation.get('pages'), list) or not presentation['pages']: raise ValueError('invalid score bundle; regenerate score assets') if (presentation.get('engine'), presentation.get('width'), presentation.get('height')) != (generator.engraving.validation.ENGINE_NAME, generator.engraving.validation.PAGE_WIDTH, generator.engraving.validation.PAGE_HEIGHT): raise ValueError('unsupported score presentation; regenerate score assets') generator.performance.validate_views(document['music'], presentation['pages'], presentation.get('playback_pages')) if document.get('performance') != generator.performance.timing(document['music']): raise ValueError('invalid performance timing') for page in presentation['pages'] + presentation['playback_pages']: if not isinstance(page, dict) or not isinstance(page.get('path'), str) or not isinstance(page.get('sha256'), str): raise ValueError('invalid score page manifest') name = page['path'] relative = pathlib.Path(name) if '\x00' in name or '\\' in name or relative.is_absolute() or any(part in ('', '.', '..') for part in name.split('/')) or relative.suffix != '.png': raise ValueError('invalid score page path') resolved = (path.parent / relative).resolve() if not resolved.is_relative_to(path.parent.resolve()): raise ValueError('score page escapes its asset directory') generator.engraving.validation.check_page(resolved, validated_pages, expected_sha256=page['sha256']) document = document['music'] generator.validation.validate_document(document) return document def write_json(path: pathlib.Path, document: dict) -> None: generator.validation.validate_document(document) generator.fretboard.validate_document(document) path.parent.mkdir(parents=True, exist_ok=True) path.write_text(json.dumps(document, indent=2, ensure_ascii=False) + '\n', encoding='utf-8') def count_json_files(root: pathlib.Path) -> int: return sum(1 for p in root.rglob('*.json') if p.is_file()) def validate_generated_tree(root: pathlib.Path, validated_pages: set[str] | None = None) -> None: validated_pages = set() if validated_pages is None else validated_pages for path in root.rglob('*.json'): document = json.loads(path.read_text(encoding='utf-8')) music = validate_json(document, path, validated_pages) generator.fretboard.validate_document(music) if 'presentation' in document: generator.pico_artwork.validate_title(root, document['presentation'].get('title_image'))