From 02c212e4a6b8efb22018e399bf6221dd8d3e0725 Mon Sep 17 00:00:00 2001 From: Salad Dais Date: Wed, 5 May 2021 04:14:03 +0000 Subject: [PATCH] Highlight matched line when matching on specific var values Very helpful for debugging ObjectUpdates which are high frequency and have many diff objects in a single message. Just the first line of the var for now. Need to be smarter about how we build the blocks in the message text if we want to highlight the whole thing. --- README.md | 2 -- hippolyzer/apps/proxy_gui.py | 17 ++++++++++++++++- hippolyzer/lib/proxy/message.py | 22 +++++++++++++++++++--- hippolyzer/lib/proxy/message_filter.py | 13 ++++++++----- hippolyzer/lib/proxy/message_logger.py | 14 +++++++++----- 5 files changed, 52 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 0c525fa..9010bac 100644 --- a/README.md +++ b/README.md @@ -301,8 +301,6 @@ If you are a viewer developer, please put them in a viewer. * AISv3 wrapper? * Higher level wrappers for common things? I don't really need these, so only if people want to write them. -* Highlight matched portion of message in log view, if applicable -* * Remember deep filters and return a map of them, have message formatter return text ranges? * Move things out of `templates.py`, right now most binary serialization stuff lives there because it's more convenient for me to hot-reload. * Ability to add menus? diff --git a/hippolyzer/apps/proxy_gui.py b/hippolyzer/apps/proxy_gui.py index d105ec4..26e1d52 100644 --- a/hippolyzer/apps/proxy_gui.py +++ b/hippolyzer/apps/proxy_gui.py @@ -35,7 +35,7 @@ from hippolyzer.lib.proxy.ca_utils import setup_ca_everywhere from hippolyzer.lib.proxy.caps_client import CapsClient from hippolyzer.lib.proxy.http_proxy import create_proxy_master, HTTPFlowContext from hippolyzer.lib.proxy.packets import Direction -from hippolyzer.lib.proxy.message import ProxiedMessage, VerbatimHumanVal, proxy_eval +from hippolyzer.lib.proxy.message import ProxiedMessage, VerbatimHumanVal, proxy_eval, SpannedString from hippolyzer.lib.proxy.message_logger import LLUDPMessageLogEntry, AbstractMessageLogEntry from hippolyzer.lib.proxy.region import ProxiedRegion from hippolyzer.lib.proxy.sessions import Session, SessionManager @@ -161,6 +161,8 @@ class ProxyGUI(QtWidgets.QMainWindow): "ViewerAsset GetTexture SetAlwaysRun GetDisplayNames MapImageService MapItemReply".split(" ") DEFAULT_FILTER = f"!({' || '.join(ignored for ignored in DEFAULT_IGNORE)})" + textRequest: QtWidgets.QTextEdit + def __init__(self): super().__init__() loadUi(MAIN_WINDOW_UI_PATH, self) @@ -263,6 +265,12 @@ class ProxyGUI(QtWidgets.QMainWindow): beautify=self.checkBeautify.isChecked(), replacements=self.buildReplacements(entry.session, entry.region), ) + highlight_range = None + if isinstance(req, SpannedString): + match_result = self.model.filter.match(entry) + # Match result was a tuple indicating what matched + if isinstance(match_result, tuple): + highlight_range = req.spans.get(match_result) resp = entry.response(beautify=self.checkBeautify.isChecked()) self.textRequest.setPlainText(req) if resp: @@ -271,6 +279,13 @@ class ProxyGUI(QtWidgets.QMainWindow): else: self.textResponse.hide() + if highlight_range: + cursor = self.textRequest.textCursor() + cursor.setPosition(highlight_range[0], QtGui.QTextCursor.KeepAnchor) + highlight_format = QtGui.QTextBlockFormat() + highlight_format.setBackground(QtCore.Qt.yellow) + cursor.setBlockFormat(highlight_format) + def beforeInsert(self): vbar = self.tableView.verticalScrollBar() self._shouldScrollOnInsert = vbar.value() == vbar.maximum() diff --git a/hippolyzer/lib/proxy/message.py b/hippolyzer/lib/proxy/message.py index f21622b..b29c8fe 100644 --- a/hippolyzer/lib/proxy/message.py +++ b/hippolyzer/lib/proxy/message.py @@ -5,6 +5,7 @@ import logging import math import os import re +import typing import uuid from typing import * @@ -71,6 +72,14 @@ def proxy_eval(eval_str: str, globals_=None, locals_=None): ) +TextSpan = Tuple[int, int] +SpanDict = Dict[Tuple[Union[str, int], ...], TextSpan] + + +class SpannedString(str): + spans: SpanDict = {} + + class ProxiedMessage(Message): __slots__ = ("meta", "injected", "dropped", "direction") @@ -83,9 +92,10 @@ class ProxiedMessage(Message): _maybe_reload_templates() def to_human_string(self, replacements=None, beautify=False, - template: Optional[MessageTemplate] = None): + template: Optional[MessageTemplate] = None) -> SpannedString: replacements = replacements or {} _maybe_reload_templates() + spans: SpanDict = {} string = "" if self.direction is not None: string += f'{self.direction.name} ' @@ -101,11 +111,17 @@ class ProxiedMessage(Message): block_suffix = "" if template and template.get_block(block_name).block_type == MsgBlockType.MBT_VARIABLE: block_suffix = ' # Variable' - for block in block_list: + for block_num, block in enumerate(block_list): string += f"[{block_name}]{block_suffix}\n" for var_name, val in block.items(): + start_len = len(string) string += self._format_var(block, var_name, val, replacements, beautify) - return string + end_len = len(string) + # Store the spans for each var so we can highlight specific matches + spans[(self.name, block_name, block_num, var_name)] = (start_len, end_len) + spanned = SpannedString(string) + spanned.spans = spans + return spanned def _format_var(self, block, var_name, var_val, replacements, beautify=False): string = "" diff --git a/hippolyzer/lib/proxy/message_filter.py b/hippolyzer/lib/proxy/message_filter.py index 666878e..4777204 100644 --- a/hippolyzer/lib/proxy/message_filter.py +++ b/hippolyzer/lib/proxy/message_filter.py @@ -62,9 +62,12 @@ def message_filter(): return expression, EOF +MATCH_RESULT = typing.Union[bool, typing.Tuple] + + class BaseFilterNode(abc.ABC): @abc.abstractmethod - def match(self, msg) -> bool: + def match(self, msg) -> MATCH_RESULT: raise NotImplementedError() @property @@ -94,17 +97,17 @@ class BinaryFilterNode(BaseFilterNode, abc.ABC): class UnaryNotFilterNode(UnaryFilterNode): - def match(self, msg) -> bool: + def match(self, msg) -> MATCH_RESULT: return not self.node.match(msg) class OrFilterNode(BinaryFilterNode): - def match(self, msg) -> bool: + def match(self, msg) -> MATCH_RESULT: return self.left_node.match(msg) or self.right_node.match(msg) class AndFilterNode(BinaryFilterNode): - def match(self, msg) -> bool: + def match(self, msg) -> MATCH_RESULT: return self.left_node.match(msg) and self.right_node.match(msg) @@ -114,7 +117,7 @@ class MessageFilterNode(BaseFilterNode): self.operator = operator self.value = value - def match(self, msg) -> bool: + def match(self, msg) -> MATCH_RESULT: return msg.matches(self) @property diff --git a/hippolyzer/lib/proxy/message_logger.py b/hippolyzer/lib/proxy/message_logger.py index 7501b8a..45266f1 100644 --- a/hippolyzer/lib/proxy/message_logger.py +++ b/hippolyzer/lib/proxy/message_logger.py @@ -586,15 +586,19 @@ class LLUDPMessageLogEntry(AbstractMessageLogEntry): for block_name in message.blocks: if not fnmatch.fnmatchcase(block_name, matcher.selector[1]): continue - for block in message[block_name]: + for block_num, block in enumerate(message[block_name]): for var_name in block.vars.keys(): if not fnmatch.fnmatchcase(var_name, matcher.selector[2]): continue + # So we know where the match happened + span_key = (message.name, block_name, block_num, var_name) if selector_len == 3: + # We're just matching on the var existing, not having any particular value if matcher.value is None: - return True + return span_key if self._val_matches(matcher.operator, block[var_name], matcher.value): - return True + return span_key + # Need to invoke a special unpacker elif selector_len == 4: try: deserialized = block.deserialize_var(var_name) @@ -608,9 +612,9 @@ class LLUDPMessageLogEntry(AbstractMessageLogEntry): for key in deserialized.keys(): if fnmatch.fnmatchcase(str(key), matcher.selector[3]): if matcher.value is None: - return True + return span_key if self._val_matches(matcher.operator, deserialized[key], matcher.value): - return True + return span_key return False