87 lines
3.6 KiB
Python
87 lines
3.6 KiB
Python
"""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,
|
|
}
|