Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 21 additions & 7 deletions aw_transform/classify.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import logging
from typing import Pattern, List, Iterable, Tuple, Dict, Optional, Any
from functools import reduce
import re

from aw_core import Event


logger = logging.getLogger(__name__)

Tag = str
Category = List[str]

Expand All @@ -20,13 +23,24 @@ def __init__(self, rules: Dict[str, Any]) -> None:

# NOTE: Also checks that the regex isn't an empty string (which would erroneously match everything)
regex_str = rules.get("regex", None)
self.regex = (
re.compile(
regex_str, (re.IGNORECASE if self.ignore_case else 0) | re.UNICODE
)
if regex_str
else None
)
if regex_str:
try:
self.regex = re.compile(
regex_str,
(re.IGNORECASE if self.ignore_case else 0) | re.UNICODE,
)
except re.error as e:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Non-string regex errors escape

When a categorize or tag rule contains a truthy non-string regex value such as 123, re.compile raises TypeError, which bypasses this handler and still aborts the entire query with an internal server error.

Suggested change
except re.error as e:
except (re.error, TypeError) as e:

# An invalid user-supplied pattern should not crash categorization
# for the entire query. Log it and disable this rule (match nothing).
logger.warning(
"Invalid regex pattern %r in category/tag rule (%s); "
"this rule will match nothing.",
regex_str,
e,
)
self.regex = None
else:
self.regex = None

def match(self, e: Event) -> bool:
if self.select_keys:
Expand Down
29 changes: 29 additions & 0 deletions tests/test_transforms.py
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,35 @@ def test_tags():
assert len(events[1].data["$tags"]) == 0


def test_rule_invalid_regex_does_not_raise():
# An invalid user-supplied pattern (e.g. "Notepad++" on Python versions
# where "++" is not a valid quantifier) must not raise from Rule.__init__.
# Instead the rule should silently disable itself. See:
# https://github.com/ActivityWatch/activitywatch/issues/1340
rule = Rule({"regex": "*invalid("})
assert rule.regex is None

now = datetime.now(timezone.utc)
e = Event(timestamp=now, duration=0, data={"key": "anything"})
assert rule.match(e) is False


def test_categorize_survives_invalid_regex():
# A single bad rule should not break categorization for the rest.
now = datetime.now(timezone.utc)
classes = [
(["Bad"], Rule({"regex": "*invalid("})),
(["Test"], Rule({"regex": "^just"})),
]
events = [
Event(timestamp=now, duration=0, data={"key": "just a test"}),
Event(timestamp=now, duration=0, data={"key": "unrelated"}),
]
events = categorize(events, classes)
assert events[0].data["$category"] == ["Test"]
assert events[1].data["$category"] == ["Uncategorized"]


def test_union_no_overlap():
from pprint import pprint

Expand Down
Loading