open-standards/aac/aac-rs/tools/symbols/check_packs_nc.py

111 lines
5 KiB
Python

#!/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("<I", blob[8:12])
header = json.loads(blob[12:12 + n].decode("utf-8"))
data_len = len(blob) - 12 - n
if header.get("set") != set_id:
problems.append("{}: a pack for {!r}".format(path.name, header.get("set")))
mapped = {by_set[set_id] for by_set in index["concepts"].values() if set_id in by_set}
held = set(header.get("symbols", {}))
if mapped != held:
problems.append("{}: {} symbols the catalogue maps are not in it, {} in it are not mapped".format(
path.name, len(mapped - held), len(held - mapped)))
for sid, e in header.get("symbols", {}).items():
for part in (e.get("image"), e.get("skin")):
if part is not None and (part[0] < 0 or part[1] <= 0 or part[0] + part[1] > 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))