"""SAM v3.3 text protocol parser and formatter. Ported from net.i2p.sam.SAMUtils and related classes. """ from __future__ import annotations from i2p_sam.utils import parse_params # SAM protocol constants SUPPORTED_VERSIONS = ["3.0", "3.1", "3.2", "3.3"] MAX_VERSION = "3.3" MIN_VERSION = "3.0" class SAMCommand: """Parsed SAM command with verb, opcode, and key=value params. SAM protocol messages have the form: VERB OPCODE KEY=VALUE KEY=VALUE ... For example: HELLO VERSION MIN=3.0 MAX=3.3 SESSION CREATE ID=test STYLE=STREAM DESTINATION=TRANSIENT """ def __init__(self, verb: str, opcode: str, params: dict[str, str]) -> None: self.verb = verb self.opcode = opcode self.params = params @classmethod def parse(cls, line: str) -> "SAMCommand": """Parse 'VERB OPCODE KEY=VALUE KEY=VALUE ...' text line. Values may be quoted. Keys are case-insensitive (normalized to uppercase). Args: line: Raw text line from SAM protocol. Returns: Parsed SAMCommand instance. Raises: ValueError: If the line is empty or cannot be parsed. """ stripped = line.strip() if not stripped: raise ValueError("Empty SAM command") parts = stripped.split(None, 2) # Split into at most 3 parts verb = parts[0].upper() if len(parts) == 1: return cls(verb, "", {}) opcode = parts[1] if len(parts) == 2: # Could be VERB OPCODE with no params, or opcode might contain = if "=" in opcode: # It's actually a param, not an opcode params = parse_params(opcode) return cls(verb, "", params) return cls(verb, opcode, {}) # parts[2] contains the rest (params) remainder = parts[2] # Check if opcode contains = (meaning it's actually a param) if "=" in opcode: params = parse_params(opcode + " " + remainder) return cls(verb, "", params) params = parse_params(remainder) return cls(verb, opcode, params) def __str__(self) -> str: """Format back to SAM text protocol.""" parts = [self.verb] if self.opcode: parts.append(self.opcode) for key, value in self.params.items(): if " " in value: parts.append(f'{key}="{value}"') else: parts.append(f"{key}={value}") return " ".join(parts) def __repr__(self) -> str: return f"SAMCommand({self.verb!r}, {self.opcode!r}, {self.params!r})" class SAMReply: """Format SAM reply messages. All reply methods return newline-terminated strings ready to send over the wire. """ @staticmethod def hello_ok(version: str = "3.3") -> str: """HELLO REPLY with OK result.""" return f"HELLO REPLY RESULT=OK VERSION={version}\n" @staticmethod def hello_noversion() -> str: """HELLO REPLY with NOVERSION result.""" return "HELLO REPLY RESULT=NOVERSION\n" @staticmethod def session_ok(destination: str) -> str: """SESSION STATUS with OK result and destination.""" return f"SESSION STATUS RESULT=OK DESTINATION={destination}\n" @staticmethod def session_error(result: str, message: str = "") -> str: """SESSION STATUS with error result. Args: result: Error code (e.g. DUPLICATED_ID, INVALID_KEY). message: Optional human-readable message. """ msg = f"SESSION STATUS RESULT={result}" if message: msg += f" MESSAGE=\"{message}\"" return msg + "\n" @staticmethod def stream_ok() -> str: """STREAM STATUS with OK result.""" return "STREAM STATUS RESULT=OK\n" @staticmethod def stream_error(result: str, message: str = "") -> str: """STREAM STATUS with error result. Args: result: Error code (e.g. CANT_REACH_PEER, TIMEOUT). message: Optional human-readable message. """ msg = f"STREAM STATUS RESULT={result}" if message: msg += f" MESSAGE=\"{message}\"" return msg + "\n" @staticmethod def dest_reply(name: str, destination: str) -> str: """DEST REPLY with found destination.""" return f"DEST REPLY RESULT=OK NAME={name} DESTINATION={destination}\n" @staticmethod def dest_not_found(name: str) -> str: """DEST REPLY with KEY_NOT_FOUND result.""" return f"DEST REPLY RESULT=KEY_NOT_FOUND NAME={name}\n" @staticmethod def naming_reply(name: str, value: str) -> str: """NAMING REPLY with resolved name.""" return f"NAMING REPLY RESULT=OK NAME={name} VALUE={value}\n" @staticmethod def naming_not_found(name: str) -> str: """NAMING REPLY with KEY_NOT_FOUND result.""" return f"NAMING REPLY RESULT=KEY_NOT_FOUND NAME={name}\n" @staticmethod def pong(data: str) -> str: """PONG response to PING.""" return f"PONG {data}\n"