Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 252470@bnt-lap001 Assistant-Session: ad628c8f-4be2-4979-94c9-76c7ed6f8241
130 lines
4.3 KiB
Python
Executable file
130 lines
4.3 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
"""Non-normative MAS v0.1 canonical ASCII reference parser."""
|
|
|
|
import argparse
|
|
import json
|
|
import re
|
|
from pathlib import Path
|
|
|
|
ATOM = set("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789#%&'*-/ ?_`{|}~".replace(" ", ""))
|
|
RTYPE = re.compile(r"[a-z0-9]+(?:[_-][a-z0-9]+)*") # v0.1.1: lower-case, single separators
|
|
RESERVED = set("+!$^")
|
|
|
|
def _valid_dot_string(s, allowed):
|
|
if not s or s.startswith(".") or s.endswith(".") or ".." in s:
|
|
return False
|
|
return all((c == "." or c in allowed) for c in s)
|
|
|
|
def parse(local_part, profile="p1"):
|
|
if not isinstance(local_part, str) or not local_part:
|
|
raise ValueError("empty local-part")
|
|
try:
|
|
local_part.encode("ascii")
|
|
except UnicodeEncodeError:
|
|
raise ValueError("non-ASCII outside canonical MAS v0.1")
|
|
|
|
if '"' in local_part or "\\" in local_part or "@" in local_part:
|
|
raise ValueError("quoted/escaped/full address outside canonical local-part grammar")
|
|
if "$" in local_part or "^" in local_part:
|
|
raise ValueError("reserved operator")
|
|
|
|
if local_part.count("+") > 1:
|
|
raise ValueError("multiple facet operators")
|
|
if local_part.count("!") > 1:
|
|
raise ValueError("multiple interaction operators")
|
|
|
|
plus = local_part.find("+")
|
|
bang = local_part.find("!")
|
|
if plus >= 0 and bang >= 0 and bang < plus:
|
|
raise ValueError("non-canonical operator order")
|
|
if profile == "p1" and bang >= 0:
|
|
raise ValueError("interaction not enabled in P1")
|
|
if profile not in ("p1", "p2"):
|
|
raise ValueError("unknown profile")
|
|
|
|
interaction = None
|
|
s = local_part
|
|
if bang >= 0:
|
|
interaction = s[bang+1:]
|
|
if not interaction:
|
|
raise ValueError("empty interaction")
|
|
s = s[:bang]
|
|
if not _valid_dot_string(interaction, ATOM | set("=")):
|
|
raise ValueError("invalid interaction")
|
|
|
|
facet = None
|
|
plus = s.find("+")
|
|
if plus >= 0:
|
|
facet = s[plus+1:]
|
|
if not facet:
|
|
raise ValueError("empty facet")
|
|
s = s[:plus]
|
|
if not _valid_dot_string(facet, ATOM | set("=")):
|
|
raise ValueError("invalid facet")
|
|
|
|
target = s
|
|
if not target:
|
|
raise ValueError("empty target")
|
|
|
|
out = {
|
|
"target": target,
|
|
"target_kind": "opaque",
|
|
"facet": facet,
|
|
"interaction": interaction,
|
|
}
|
|
|
|
if "=" in target:
|
|
rtype, rid = target.split("=", 1)
|
|
if not rtype:
|
|
raise ValueError("empty resource type")
|
|
if not rid:
|
|
raise ValueError("empty resource id")
|
|
if not RTYPE.fullmatch(rtype):
|
|
raise ValueError("invalid resource type")
|
|
if not _valid_dot_string(rid, ATOM | set("=")):
|
|
raise ValueError("invalid resource id")
|
|
out.update({
|
|
"target_kind": "resource",
|
|
"resource_type": rtype,
|
|
"resource_id": rid,
|
|
})
|
|
else:
|
|
if not _valid_dot_string(target, ATOM):
|
|
raise ValueError("invalid opaque target")
|
|
|
|
if len(local_part.encode("ascii")) > 64:
|
|
raise ValueError("local-part exceeds 64 octets")
|
|
|
|
return out
|
|
|
|
def self_test():
|
|
here = Path(__file__).resolve().parents[1]
|
|
fixtures = json.loads((here / "conformance" / "addresses.json").read_text())
|
|
failures = []
|
|
for f in fixtures:
|
|
try:
|
|
got = parse(f["local_part"], f["profile"])
|
|
if not f["valid"]:
|
|
failures.append((f["id"], "expected invalid", got))
|
|
elif got != f["parsed"]:
|
|
failures.append((f["id"], "parse mismatch", {"got":got, "expected":f["parsed"]}))
|
|
except ValueError as e:
|
|
if f["valid"]:
|
|
failures.append((f["id"], "unexpected invalid", str(e)))
|
|
if failures:
|
|
print(json.dumps(failures, indent=2))
|
|
raise SystemExit(1)
|
|
print(f"PASS: {len(fixtures)} MAS v0.1 fixtures")
|
|
|
|
if __name__ == "__main__":
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("local_part", nargs="?")
|
|
ap.add_argument("--profile", choices=["p1","p2"], default="p1")
|
|
ap.add_argument("--self-test", action="store_true")
|
|
args = ap.parse_args()
|
|
if args.self_test:
|
|
self_test()
|
|
elif args.local_part:
|
|
print(json.dumps(parse(args.local_part, args.profile), indent=2))
|
|
else:
|
|
ap.print_help()
|