"""Bounded fingerprints of received argument deltas, without retaining text.""" import hashlib PREFIX_BYTES = 4096 MAX_PREFIXES = 16 MAX_TRACKED_TOOLS = 64 class ArgumentFingerprint: def __init__(self): self.characters = 0 self.bytes = 0 self.deltas = 0 self.encoding_valid = True self.digest = hashlib.sha256() self.prefixes: list[dict] = [] self.whitespace_chars = 0 self.whitespace_run = 0 self.max_whitespace_run = 0 self.min_delta_chars: int | None = None self.max_delta_chars = 0 self.same_delta_run = 0 self.max_same_delta_run = 0 self.previous_delta_digest: bytes | None = None def observe(self, arguments: str) -> None: self.characters += len(arguments) if not arguments: return self.deltas += 1 length = len(arguments) self.min_delta_chars = min(self.min_delta_chars or length, length) self.max_delta_chars = max(self.max_delta_chars, length) for character in arguments: if character in " \t\r\n": self.whitespace_chars += 1 self.whitespace_run += 1 self.max_whitespace_run = max(self.max_whitespace_run, self.whitespace_run) else: self.whitespace_run = 0 # One digest, never the previous argument body. Surrogate handling here # compares SDK strings only; wire UTF-8 fingerprints remain invalid below. delta_digest = hashlib.sha256(arguments.encode("utf-8", "surrogatepass")).digest() self.same_delta_run = ( self.same_delta_run + 1 if delta_digest == self.previous_delta_digest else 1 ) self.max_same_delta_run = max(self.max_same_delta_run, self.same_delta_run) self.previous_delta_digest = delta_digest if not self.encoding_valid: return try: encoded = arguments.encode("utf-8") except UnicodeEncodeError: # Diagnostics must not repair, reject or otherwise alter the stream. self.encoding_valid = False return offset = 0 while offset < len(encoded) and len(self.prefixes) < MAX_PREFIXES: size = min(PREFIX_BYTES - self.bytes % PREFIX_BYTES, len(encoded) - offset) self.digest.update(encoded[offset : offset + size]) self.bytes += size offset += size if self.bytes % PREFIX_BYTES == 0: self.prefixes.append({"bytes": self.bytes, "sha256": self.digest.hexdigest()}) if offset < len(encoded): self.digest.update(encoded[offset:]) self.bytes += len(encoded) - offset def snapshot(self) -> dict: return { "argument_chars": self.characters, "argument_bytes": self.bytes if self.encoding_valid else None, "argument_sha256": self.digest.hexdigest() if self.encoding_valid else None, "argument_deltas": self.deltas, "argument_prefixes": [dict(prefix) for prefix in self.prefixes], "argument_encoding_valid": self.encoding_valid, "argument_json_whitespace_chars": self.whitespace_chars, "argument_json_whitespace_run": self.whitespace_run, "argument_json_whitespace_run_max": self.max_whitespace_run, "argument_delta_chars_min": self.min_delta_chars, "argument_delta_chars_max": self.max_delta_chars, "argument_same_delta_run": self.same_delta_run, "argument_same_delta_run_max": self.max_same_delta_run, }