#!/usr/bin/env python3 """Check a directory of optional symbol packs against the app's catalogue. The optional, non-commercial sets (ARASAAC, Sclera) are built outside this repository by build_nc.py --out DIR, and a free deployment with no adverts ships DIR's packs beside the app (under /app/models/symbols/). The app knows each pack's file name, size and SHA-256 from index-nc.json, compiled in; this checks that DIR holds exactly those packs, whole, and that each pack holds exactly the symbols the catalogue maps for its set. Standard library only. Usage: python3 tools/symbols/check_packs_nc.py DIR [--sets arasaac,sclera] Prints the sets checked, comma-separated, on the last line (for AAC_SYMBOL_PACKS); exits 1 on any difference. """ import hashlib import json import struct import sys from pathlib import Path HERE = Path(__file__).resolve().parent INDEX = HERE.parents[1] / "crates" / "aac-core" / "src" / "symbols" / "index-nc.json" MAGIC = b"AACSYMP1" def main(argv): if len(argv) < 2: print(__doc__.strip().splitlines()[-3], file=sys.stderr) return 2 folder = Path(argv[1]) index = json.loads(INDEX.read_text(encoding="utf-8")) sets = list(index["sets"]) if "--sets" in argv: wanted = [s for s in argv[argv.index("--sets") + 1].split(",") if s] unknown = [s for s in wanted if s not in index["sets"]] if unknown: print("unknown sets: " + ", ".join(unknown), file=sys.stderr) return 1 sets = wanted problems = [] named = set() for set_id in sets: info = index["sets"][set_id] named.add(info["file"]) path = folder / info["file"] if not path.is_file(): problems.append("{}: {} is missing".format(set_id, path)) continue blob = path.read_bytes() if len(blob) != info["size"]: problems.append("{}: {} bytes, the catalogue says {}".format(path.name, len(blob), info["size"])) digest = hashlib.sha256(blob).hexdigest() if digest != info["sha256"]: problems.append("{}: SHA-256 {}, the catalogue says {}".format(path.name, digest, info["sha256"])) continue if blob[:8] != MAGIC: problems.append("{}: not a symbol pack".format(path.name)) continue (n,) = struct.unpack(" data_len): problems.append("{}: {} points outside the pack".format(path.name, sid)) if not header.get("license", {}).get("spdx") == info["license"]["spdx"]: problems.append("{}: its licence is not the catalogue's".format(path.name)) licence = folder / "LICENSE.md" if not licence.is_file(): problems.append("{} is missing: the packs travel with their licence (build_nc.py writes it)".format(licence)) else: text = licence.read_text(encoding="utf-8") for set_id in sets: info = index["sets"][set_id] if info["file"] not in text or info["attribution"] not in text: problems.append("{}: not this catalogue's (no {} or its attribution)".format(licence, info["file"])) # Their section of the app's symbols/LICENSES.md, appended by a build # that ships them (the general file never names them). section = folder / "LICENSES-nc.md" if not section.is_file(): problems.append("{} is missing (build_nc.py writes it)".format(section)) else: text = section.read_text(encoding="utf-8") for set_id in sets: info = index["sets"][set_id] if info["file"] not in text or info["attribution"] not in text: problems.append("{}: not this catalogue's (no {} or its attribution)".format(section, info["file"])) stale = sorted(p.name for p in folder.glob("*.pack") if p.name not in named) if folder.is_dir() else [] if stale: problems.append("packs the catalogue does not name (remove them): " + ", ".join(stale)) for p in problems: print("✗ " + p, file=sys.stderr) if problems: return 1 for set_id in sets: info = index["sets"][set_id] print("✓ {} {} ({:,} bytes)".format(set_id, info["file"], info["size"]), file=sys.stderr) print(",".join(sets)) return 0 if __name__ == "__main__": sys.exit(main(sys.argv))