"""Share link encoding/decoding — base64-encodes compressed plan JSON for URL sharing.""" import base64 import json import zlib from urllib.parse import parse_qs, urlparse from core.planner import ProjectPlan def encode_plan(plan: ProjectPlan) -> str: """ Encode a ProjectPlan to a URL-safe base64 string. Process: model_dump(exclude_none=True) -> json.dumps -> zlib.compress -> base64 urlsafe encode Returns encoded string, or empty string on failure. """ try: plan_dict = plan.model_dump(exclude_none=True) json_bytes = json.dumps(plan_dict, separators=(",", ":")).encode("utf-8") compressed = zlib.compress(json_bytes) token = base64.urlsafe_b64encode(compressed).decode("ascii") return token except Exception: return "" def decode_plan(token: str) -> ProjectPlan | None: """ Decode a share token back to a ProjectPlan. Process: base64 urlsafe decode -> zlib.decompress -> json.loads -> parse_plan_json Returns ProjectPlan on success, None on any failure. """ try: compressed = base64.urlsafe_b64decode(token.encode("ascii")) json_bytes = zlib.decompress(compressed) plan_dict = json.loads(json_bytes.decode("utf-8")) # plan_dict is already a dict — use model_validate directly return ProjectPlan.model_validate(plan_dict) except Exception: return None def build_share_url(base_url: str, plan: ProjectPlan) -> str: """ Build a full share URL: {base_url}?share={encoded_token} Returns empty string if encoding fails. """ token = encode_plan(plan) if not token: return "" separator = "&" if "?" in base_url else "?" return f"{base_url}{separator}share={token}" def parse_share_url(url: str) -> ProjectPlan | None: """ Extract and decode the ?share= token from a URL. Returns ProjectPlan or None. """ try: parsed = urlparse(url) params = parse_qs(parsed.query) tokens = params.get("share", []) if not tokens: return None return decode_plan(tokens[0]) except Exception: return None