Coverage for documents/regex.py: 30%
54 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 09:07 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 09:07 +0000
1from __future__ import annotations
3import logging
4import textwrap
6import regex
7from django.conf import settings
9logger = logging.getLogger("paperless.regex")
11REGEX_TIMEOUT_SECONDS: float = getattr(settings, "MATCH_REGEX_TIMEOUT_SECONDS", 0.1)
14def validate_regex_pattern(pattern: str) -> None:
15 """
16 Validate user provided regex for basic compile errors.
17 Raises ValueError on validation failure.
18 """
20 try:
21 regex.compile(pattern)
22 except regex.error as exc:
23 raise ValueError(exc.msg) from exc
26def safe_regex_search(pattern: str, text: str, *, flags: int = 0):
27 """
28 Run a regex search with a timeout. Returns a match object or None.
29 Validation errors and timeouts are logged and treated as no match.
30 """
32 try:
33 validate_regex_pattern(pattern)
34 compiled = regex.compile(pattern, flags=flags)
35 except (regex.error, ValueError) as exc:
36 logger.error(
37 "Error while processing regular expression %s: %s",
38 textwrap.shorten(pattern, width=80, placeholder="…"),
39 exc,
40 )
41 return None
43 try:
44 return compiled.search(text, timeout=REGEX_TIMEOUT_SECONDS)
45 except TimeoutError:
46 logger.warning(
47 "Regular expression matching timed out for pattern %s",
48 textwrap.shorten(pattern, width=80, placeholder="…"),
49 )
50 return None
53def safe_regex_match(pattern: str, text: str, *, flags: int = 0):
54 """
55 Run a regex match with a timeout. Returns a match object or None.
56 Validation errors and timeouts are logged and treated as no match.
57 """
59 try:
60 validate_regex_pattern(pattern)
61 compiled = regex.compile(pattern, flags=flags)
62 except (regex.error, ValueError) as exc:
63 logger.exception(
64 "Error while processing regular expression %s: %s",
65 textwrap.shorten(pattern, width=80, placeholder="…"),
66 exc,
67 )
68 return None
70 try:
71 return compiled.match(text, timeout=REGEX_TIMEOUT_SECONDS)
72 except TimeoutError:
73 logger.warning(
74 "Regular expression matching timed out for pattern %s",
75 textwrap.shorten(pattern, width=80, placeholder="…"),
76 )
77 return None
80def safe_regex_sub(pattern: str, repl: str, text: str, *, flags: int = 0) -> str | None:
81 """
82 Run a regex substitution with a timeout. Returns the substituted string,
83 or None on error/timeout.
84 """
86 try:
87 validate_regex_pattern(pattern)
88 compiled = regex.compile(pattern, flags=flags)
89 except (regex.error, ValueError) as exc:
90 logger.exception(
91 "Error while processing regular expression %s: %s",
92 textwrap.shorten(pattern, width=80, placeholder="…"),
93 exc,
94 )
95 return None
97 try:
98 return compiled.sub(repl, text, timeout=REGEX_TIMEOUT_SECONDS)
99 except TimeoutError:
100 logger.warning(
101 "Regular expression substitution timed out for pattern %s",
102 textwrap.shorten(pattern, width=80, placeholder="…"),
103 )
104 return None
107def safe_regex_finditer(compiled_pattern: regex.Pattern, text: str):
108 """
109 Run regex finditer with a timeout. Yields match objects.
110 Stops iteration on timeout.
111 """
113 try:
114 yield from compiled_pattern.finditer(text, timeout=REGEX_TIMEOUT_SECONDS)
115 except TimeoutError:
116 logger.warning(
117 "Regular expression finditer timed out for pattern %s",
118 textwrap.shorten(compiled_pattern.pattern, width=80, placeholder="…"),
119 )
120 return