111 lines
5 KiB
Python
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))
|