1"""
2This module provides private data for the base implementation for the
3:module:`hyperscan` library.
4
5WARNING: The *pathspec._backends.hyperscan* package is not part of the public
6API. Its contents and structure are likely to change.
7"""
8from __future__ import annotations
9
10import re
11from dataclasses import (
12 dataclass)
13from typing import (
14 Union, # Replaced by `X | Y` in 3.10.
15 cast)
16
17try:
18 import hyperscan
19except ModuleNotFoundError:
20 hyperscan = None # type: ignore[assignment]
21 HS_FLAGS = 0
22 HS_VERSION = (-1, -1, -1)
23else:
24 HS_FLAGS = hyperscan.HS_FLAG_SINGLEMATCH | hyperscan.HS_FLAG_UTF8
25 HS_VERSION = cast(tuple[int, int, int], tuple(map(int, hyperscan.__version__.split('.')[:3]))) # type: ignore[attr-defined]
26
27_COMPAT_CONVERT_UNICODE = re.compile(r'\\(.)|([^\x00-\x7F])', re.DOTALL)
28"""
29Regex to match non-ASCII characters, and escaped characters.
30"""
31
32HS_FLAGS: int # type: ignore[no-redef]
33"""
34The hyperscan flags to use:
35
36- HS_FLAG_SINGLEMATCH is needed to ensure the partial patterns only match once.
37
38- HS_FLAG_UTF8 is required to support unicode paths.
39"""
40
41HS_VERSION: tuple[int, int, int] # type: ignore[no-redef]
42"""
43The hyperscan version tuple.
44"""
45
46HS_VERSION_0_9_1 = (0, 9, 1)
47"""
48The hyperscan version tuple for version 0.9.1.
49"""
50
51
52def compat_convert_utf8(regex: str) -> bytes:
53 """
54 Encode unicode characters to an encoded representation. This is needed because
55 hyperscan versions before 0.9.1 on Linux fail on multibyte UTF-8 characters.
56
57 See <https://github.com/darvid/python-hyperscan/issues/274>.
58
59 *regex* (:class:`str`) is the unicode regex.
60
61 Returns the ASCII regex (:class:`bytes`).
62 """
63 return _COMPAT_CONVERT_UNICODE.sub(_replace_unicode, regex).encode('ascii')
64
65
66def _encode_uchar(uchar: str) -> str:
67 """
68 Encode the unicode character.
69
70 *uchar* (:class:`str`) is the unicode character.
71
72 Returns the encoded string (:class:`str`).
73 """
74 cp = ord(uchar)
75 if cp <= 0xFF:
76 return f'\\x{{{cp:02X}}}'
77 else:
78 return f'\\x{{{cp:04X}}}'
79
80
81def _replace_unicode(match: re.Match) -> str:
82 """
83 Replaces the matched unicode character with an encoded representation, handling
84 potential invalid byte sequences gracefully by substituting them with the
85 replacement character.
86
87 *match* (:class:`re.Match`) is the match object.
88
89 Returns the string replacement (:class:`str`).
90 """
91 escaped, uchar = match.groups()
92 if uchar is not None:
93 # Unicode character.
94 return _encode_uchar(uchar)
95 elif escaped.isascii():
96 # Escaped ASCII character.
97 return match[0]
98 else:
99 # Escaped unicode character.
100 return _encode_uchar(escaped)
101
102
103@dataclass(frozen=True)
104class HyperscanExprDat(object):
105 """
106 The :class:`HyperscanExprDat` class is used to store data related to an
107 expression.
108 """
109
110 # The slots argument is not supported until Python 3.10.
111 __slots__ = [
112 'include',
113 'index',
114 'is_dir_pattern',
115 ]
116
117 include: bool
118 """
119 *include* (:class:`bool`) is whether is whether the matched files should be
120 included (:data:`True`), or excluded (:data:`False`).
121 """
122
123 index: int
124 """
125 *index* (:class:`int`) is the pattern index.
126 """
127
128 is_dir_pattern: bool
129 """
130 *is_dir_pattern* (:class:`bool`) is whether the pattern is a directory
131 pattern for gitignore.
132 """
133
134
135@dataclass(frozen=True)
136class HyperscanExprDebug(HyperscanExprDat):
137 """
138 The :class:`HyperscanExprDebug` class stores additional debug information
139 related to an expression.
140 """
141
142 # The slots argument is not supported until Python 3.10.
143 __slots__ = ['regex']
144
145 regex: Union[str, bytes]
146 """
147 *regex* (:class:`str` or :class:`bytes`) is the regular expression.
148 """