Coverage for documents/regex.py: 30%

54 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-10-10 09:07 +0000

1from __future__ import annotations 

2 

3import logging 

4import textwrap 

5 

6import regex 

7from django.conf import settings 

8 

9logger = logging.getLogger("paperless.regex") 

10 

11REGEX_TIMEOUT_SECONDS: float = getattr(settings, "MATCH_REGEX_TIMEOUT_SECONDS", 0.1) 

12 

13 

14def validate_regex_pattern(pattern: str) -> None: 

15 """ 

16 Validate user provided regex for basic compile errors. 

17 Raises ValueError on validation failure. 

18 """ 

19 

20 try: 

21 regex.compile(pattern) 

22 except regex.error as exc: 

23 raise ValueError(exc.msg) from exc 

24 

25 

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 """ 

31 

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 

42 

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 

51 

52 

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 """ 

58 

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 

69 

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 

78 

79 

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 """ 

85 

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 

96 

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 

105 

106 

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 """ 

112 

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