Skip to content
Merged
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
19 changes: 13 additions & 6 deletions fred/cogs/crashes.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@
from zipfile import ZipFile

import re2
import re as regex_fallback

re2.set_fallback_notification(re2.FALLBACK_WARNING)
re2.set_fallback_module(regex_fallback)

from aiohttp import ClientResponseError
from attr import dataclass
Expand All @@ -25,6 +27,9 @@
from ..libraries.common import FredCog, new_logger
from ..libraries.createembed import CrashResponse

from ..libraries.regex_util import safe_search


REGEX_LIMIT: float = 6.9
DOWNLOAD_SIZE_LIMIT = 104857600 # 100 MiB
EMOJI_CRASHES_ANALYZING = "<:FredAnalyzingFile:1283182945019891712>"
Expand Down Expand Up @@ -174,7 +179,7 @@ def formatted_chunks_of_100_mod_references() -> Generator[str, None, None]:

async def mass_regex(self, text: str) -> AsyncIterator[CrashResponse]:
for crash in config.Crashes.fetch_all():
if match := await regex_with_timeout(crash["crash"], text, flags=re2.IGNORECASE | re2.S):
if match := await safe_search(crash["crash"], text, flags=re2.IGNORECASE | re2.S):
if str(crash["response"]).startswith(self.bot.command_prefix):
if command := config.Commands.fetch(crash["response"].strip(self.bot.command_prefix)):
command_response = command["content"]
Expand All @@ -188,7 +193,7 @@ async def mass_regex(self, text: str) -> AsyncIterator[CrashResponse]:
)
else:

def replace_response_value_with_captured(m: re2.Match) -> str:
def replace_response_value_with_captured(m) -> str:
group = int(m.group(1))
if group > len(match.groups()):
return f"{{Group {group} not captured in crash regex!}}"
Expand All @@ -198,7 +203,7 @@ def replace_response_value_with_captured(m: re2.Match) -> str:
yield CrashResponse(name=crash["name"], value=response, inline=True)

async def detect_and_fetch_pastebin_content(self, text: str) -> str:
if match := re2.search(r"(https://pastebin.com/\S+)", text):
if match := await safe_search(r"(https://pastebin.com/\S+)", text):
self.logger.info("Found a pastebin link! Fetching text.")
url = re2.sub(r"(?<=bin.com)/", "/raw/", match.group(1))
async with self.bot.web_session.get(url) as response:
Expand All @@ -215,7 +220,9 @@ async def process_text(self, text: str, filename="") -> list[CrashResponse]:

responses.extend(await self.process_text(await self.detect_and_fetch_pastebin_content(text)))

if match := re2.search(r"([^\n]*Critical error:.*Engine exit[^\n]*\))", text, flags=re2.I | re2.M | re2.S):
if match := await safe_search(
r"([^\n]*Critical error:.*Engine exit[^\n]*\))", text, flags=re2.I | re2.M | re2.S
):
filename = os.path.basename(filename)
crash = match.group(1)
responses.append(
Expand Down Expand Up @@ -553,7 +560,7 @@ def _get_fg_log_details(log_file: IO[bytes]):
# It used to matter more when we were using slower regex libraries. - Borketh

lines: list[bytes] = log_file.readlines()
vanilla_info_search_area = filter(lambda l: re2.match("^LogInit", l), map(lambda b: b.decode(), lines))
vanilla_info_search_area = filter(lambda l: re2.search("^LogInit", l), map(lambda b: b.decode(), lines))

info = {}
patterns = [
Expand All @@ -578,7 +585,7 @@ def _get_fg_log_details(log_file: IO[bytes]):
logger.info("Didn't find all four pieces of information normally found in a log!")
logger.debug(json.dumps(info, indent=2))

mod_loader_logs = filter(lambda l: re2.match("LogSatisfactoryModLoader", l), map(lambda b: b.decode(), lines))
mod_loader_logs = filter(lambda l: re2.search("LogSatisfactoryModLoader", l), map(lambda b: b.decode(), lines))

for line in mod_loader_logs:
if match := re2.search(r"(?<=v\.)(?P<sml>[\d.]+)", line):
Expand Down
1 change: 1 addition & 0 deletions fred/fred_commands/_command_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
def search(
table: Type[Commands | Crashes], pattern: str, column: str, force_fuzzy: bool
) -> tuple[str | list[str], bool]:
"""Returns the top three results based on the result"""

if column not in dir(table):
raise KeyError(f"`{column}` is not a column in the {table.__name__} table!")
Expand Down
12 changes: 10 additions & 2 deletions fred/fred_commands/crashes.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,22 +102,30 @@ async def search_crashes(self, ctx: commands.Context, pattern: str, *, flags: Se
-column=(name/crash/response) The column of the database to search along. Defaults to name
Notes: Uses fuzzy matching!"""

response = get_search(config.Commands, pattern, flags.column, flags.fuzzy)
response = get_search(config.Crashes, pattern, flags.column, flags.fuzzy)
await self.bot.reply_to_msg(ctx.message, response)


def validate_crash(expression: str, response: str) -> str:
"""Returns a string describing an issue with the crash or empty string if it's fine."""
try:
print(f"Debug: Compiling expression: {expression}")
compiled = re2.compile(expression)
re2.search(compiled, "test")
print("Debug: Performing test search with re2")
re2.search(expression, "test")

replace_groups = re2.findall(r"{(\d+)}", response)
replace_groups_count = max(map(int, replace_groups), default=0)

if replace_groups_count > compiled.groups:
return f"There are replacement groups the regex does not capture!"

except (re2.error, re2.RegexError) as e:
print(f"Debug: re2 error encountered: {e}")
return f"The expression isn't valid: {e}"

except Exception as fallback_error:
print(f"Debug: Fallback module error: {fallback_error}")
return f"An error occurred in the fallback module: {fallback_error}"

return "" # all good
1 change: 1 addition & 0 deletions fred/libraries/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ async def permission_check(_ctx_or_member, *, level: int) -> bool: ...

@permission_check.register
async def _permission_check_ctx(ctx: Context, *, level: int) -> bool:

main_guild_id = config.Misc.fetch("main_guild_id")
main_guild = await ctx.bot.fetch_guild(main_guild_id)

Expand Down
23 changes: 23 additions & 0 deletions fred/libraries/regex_util.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import asyncio
import re2
import re as regex_fallback

REGEX_LIMIT: float = 6.9

re2.set_fallback_module(regex_fallback)


def pattern_uses_lookaround(pattern: str) -> bool:
return bool(regex_fallback.search(r"\(\?=|\(\?!|\(\?<=|\(\?<!", pattern))


async def safe_search(pattern: str, text: str, flags=0):
try:
return await asyncio.wait_for(asyncio.to_thread(re2.search, pattern, text, flags=flags), REGEX_LIMIT)
except asyncio.TimeoutError:
raise TimeoutError(
f"A regex timed out after {REGEX_LIMIT} seconds! \n"
f"pattern: ({pattern}) \n"
f"flags: {flags} \n"
f"on text of length {len(text)}"
)
58 changes: 22 additions & 36 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ algoliasearch = "^4.11.2"
SQLObject = "^3.12.0"
aiohttp = "^3.12.14"
semver = "^3.0.2"
pyre2-updated = "^0.3.8"
regex = "^2025"
psycopg = { extras = ["binary"], version = "^3.2.3" }
pyre2 = "^0.3.10"

[tool.poetry.group.dev.dependencies]
black = { extras = ["d"], version = "^24.8.0" }
Expand Down