"""Command-line interface for the FablePool conformance toolkit. Subcommands: * ``fpcf gen-vectors --out DIR`` write the deterministic vector suite * ``fpcf run-vectors DIR`` run a suite directory; nonzero exit on failure * ``fpcf verify OP [-c CTX ...]`` verify one wire-format file with optional context * ``fpcf canon FILE`` canonicalize a JSON file to stdout * ``fpcf keygen [--seed HEX]`` derive or generate a test keypair """ import argparse import json import os import sys from pathlib import Path from . import __version__ from .canonical import canonicalize, loads_strict from .errors import FpcfError from .ids import actor_id from .log import OpLog from .runner import run_dir from .signing import generate_keypair, keypair_from_seed, public_str from .vectors import write_suite def _cmd_gen_vectors(args) -> int: manifest = write_suite(args.out) print("wrote %d vectors to %s" % (manifest["count"], args.out)) return 0 def _cmd_run_vectors(args) -> int: try: report = run_dir(args.dir) except (RuntimeError, OSError, ValueError) as exc: print("error: could not load vectors: %s" % exc, file=sys.stderr) return 2 if args.json: print(json.dumps(report.to_dict(), indent=2, sort_keys=True)) else: for r in report.failures(): print("FAIL %-45s %s" % (r.name, r.detail)) print(report.summary()) return 0 if report.ok else 1 def _cmd_verify(args) -> int: log = OpLog() try: for ctx_path in args.context or []: log.append_raw(Path(ctx_path).read_bytes()) oid = log.append_raw(Path(args.op).read_bytes()) except FpcfError as exc: print(json.dumps({"ok": False, "error": exc.code, "detail": str(exc)})) return 1 except OSError as exc: print("error: %s" % exc, file=sys.stderr) return 2 env = log.get(oid) print(json.dumps({"ok": True, "op_id": oid, "type": env["type"]})) return 0 def _cmd_canon(args) -> int: try: obj = loads_strict(Path(args.file).read_bytes()) out = canonicalize(obj) except FpcfError as exc: print("error: %s" % exc, file=sys.stderr) return 1 except OSError as exc: print("error: %s" % exc, file=sys.stderr) return 2 sys.stdout.buffer.write(out) sys.stdout.buffer.write(b"\n") return 0 def _cmd_keygen(args) -> int: if args.seed: try: seed = bytes.fromhex(args.seed) sk = keypair_from_seed(seed) except ValueError as exc: print("error: --seed must be 64 hex chars (32 bytes): %s" % exc, file=sys.stderr) return 2 else: seed = os.urandom(32) sk = keypair_from_seed(seed) pub = public_str(sk) print( json.dumps( { "public_key": pub, "actor_id": actor_id(pub), "seed_hex": seed.hex(), "warning": "test tool only; do not reuse this seed for real identities", }, indent=2, sort_keys=True, ) ) return 0 def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( prog="fpcf", description="FablePool wire-format conformance toolkit" ) parser.add_argument("--version", action="version", version="fpcf %s" % __version__) sub = parser.add_subparsers(dest="command", required=True) p = sub.add_parser("gen-vectors", help="write the deterministic vector suite") p.add_argument("--out", default="vectors", help="output directory (default: vectors)") p.set_defaults(func=_cmd_gen_vectors) p = sub.add_parser("run-vectors", help="run a vector suite directory") p.add_argument("dir", help="directory containing vectors (and optional manifest.json)") p.add_argument("--json", action="store_true", help="emit a JSON report") p.set_defaults(func=_cmd_run_vectors) p = sub.add_parser("verify", help="verify a single wire-format operation file") p.add_argument("op", help="path to the operation's wire bytes") p.add_argument( "-c", "--context", action="append", help="path to a context op (repeatable; appended in order)", ) p.set_defaults(func=_cmd_verify) p = sub.add_parser("canon", help="canonicalize a JSON file to stdout") p.add_argument("file", help="path to a JSON file") p.set_defaults(func=_cmd_canon) p = sub.add_parser("keygen", help="derive or generate a test Ed25519 keypair") p.add_argument("--seed", help="32-byte seed as 64 hex chars (deterministic)") p.set_defaults(func=_cmd_keygen) return parser def main(argv=None) -> int: args = build_parser().parse_args(argv) return args.func(args) if __name__ == "__main__": sys.exit(main())