이번 포스팅에서는 인텔 Hyperscan 레퍼런스 가이드 번역에 이어서 Hyperscan API를 직접 사용하여 정규 표현식 패턴 매칭을 수행하는 샘플 코드를 작성하고 테스트 해보는 시간을 갖겠습니다.

해당 포스팅의 Hyperscan 설치 및 테스트 환경은 Ubuntu 18.04.5 LTS 기준입니다. 다른 OS에서 Hyperscan 테스트 코드를 작성하고자 하시는 분들은 Hyperscan 공식 홈페이지에 명시된 요구사항 정보를 확인해주시기 바랍니다.


목차

  1. 1. CMake 설치
  2. 2. Ragel 설치
  3. 3. Python 설치
  4. 4. Boost 설치
  5. 5. Hyperscan 소스 빌드
  6. 6. 테스트코드 작성
  7. 7. 정규표현식 샘플 패턴 등록
  8. 8. 빌드 및 테스트
  9. 9. 마치며

아래 그림은 Hyperscan 라이브러리를 컴파일하기 위해 미리 설치되어야할 소프트웨어 목록과 최소 버전 정보입니다. Pcap은 hyperscan에서 제공하는 샘플 코드를 사용하지 않을거라면 굳이 설치할 필요는 없기때문에 제외하도록 하겠습니다. (설치 과정에서 발생하는 오류 댓글로 남겨주시면 피드백드리겠습니다😁)


CMake 설치

1
2
$ sudo apt-get update
$ sudo apt-get install cmake

Ragel 설치

1
2
$ sudo apt-get update
$ sudo apt-get install ragel

Python 설치

1
2
$ sudo apt-get update
$ sudo apt-get install python

Boost 설치

Boost 공식 홈페이지를 참조하여 최신 버전(21.10.11 기준 1.77.0) 설치를 수행합니다.

1
2
3
4
5
$ wget https://boostorg.jfrog.io/artifactory/main/release/1.77.0/source/boost_1_77_0.tar.bz2
$ tar xvf boost_1_77_0.tar.bz2
$ cd boost_1_77_0
$ sudo ./bootstrap.sh
$ sudo ./b2 install

Hyperscan 소스 빌드

해당 링크에서 Hyperscan 최신 소스 코드(21.10.11 기준 5.4.0)를 다운로드 받으실 수 있습니다.

1
2
3
4
5
6
7
8
$ tar xvf v5.4.0.tar.gz
$ cd hyperscan-5.4.0
$ mkdir build
$ cd build
$ cmake -DBUILD_STATIC_AND_SHARED=1 ../
$ sudo make
$ sudo make install
ln -s /usr/local/include/boost <hyperscan-source-path>/include/boost

테스트코드 작성

테스트코드에 대한 설명은 코드 내 주석으로 대신하겠습니다.

Hyperscan API test code
  • cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
#include <iostream>
#include <fstream>
#include <vector>

#include <hs.h>

class HyperscanTest
{
public:
HyperscanTest()
{
db_ = nullptr;
scratch_ = nullptr;
}

~HyperscanTest()
{
hs_free_scratch(scratch_);
hs_free_database(db_);
}

// 정규표현식 샘플 패턴이 저장된 파일을 Hyperscan Pattern DB를 컴파일하기 위한 형태로 파싱해주는 함수입니다.
//
// 파싱하는 파일의 구조는 <id>:/<pattern>/<flags> 형태이며,
// id/pattern/flags를 각각 멤버 변수인 ids_, patterns_, flags_에 저장합니다.
bool parseFile(const std::string &filename)
{
std::ifstream inFile(filename, std::ifstream::in);
if (!inFile.good())
{
std::cout << "ERROR: unable to open file \"" << filename << "\"" << std::endl;
return false;
}

for (int i = 1; !inFile.eof(); i++)
{
std::string line;
getline(inFile, line);

if (line.empty() || line[0] == '#')
{
continue;
}

size_t colonIdx = line.find_first_of(':');
if (colonIdx == std::string::npos)
{
std::cout << "ERROR: Could not parse line " << i << std::endl;
inFile.close();
return false;
}

unsigned int id = std::stoi(line.substr(0, colonIdx).c_str());

const std::string expr(line.substr(colonIdx + 1));

size_t flagsStart = expr.find_last_of('/');
if (flagsStart == std::string::npos)
{
std::cout << "ERROR: no trailing '/' char" << std::endl;
inFile.close();
return false;
}

std::string pcre(expr.substr(1, flagsStart - 1));
std::string flagsStr(expr.substr(flagsStart + 1, expr.size() - flagsStart));
unsigned int flag = parseFlags(flagsStr);

patterns_.push_back(pcre);
flags_.push_back(flag);
ids_.push_back(id);
}

inFile.close();
return true;
}

// 파싱된 데이터들을 활용하여 Hyperscan에서 사용하는 Pattern DB를 컴파일하는 함수입니다.
bool compileDatabase()
{
hs_database_t *db = nullptr;
hs_compile_error_t *compileErr;

std::vector<const char *> cstrPatterns;
cstrPatterns.reserve(patterns_.size());
for (const auto &pattern : patterns_)
{
cstrPatterns.push_back(pattern.c_str());
}

hs_error_t err_compile = hs_compile_multi(cstrPatterns.data(), flags_.data(), ids_.data(), cstrPatterns.size(), HS_MODE_BLOCK, nullptr, &db, &compileErr);
if (err_compile != HS_SUCCESS)
{
if (compileErr->expression < 0)
{
std::cout << "ERROR: " << compileErr->message << std::endl;
hs_free_compile_error(compileErr);
return false;
}
else
{
std::cout << "ERROR: Pattern '" << patterns_[compileErr->expression] << "' failed with error '" << compileErr->message << "'" << std::endl;
hs_free_compile_error(compileErr);
return false;
}
}

if (db_ != nullptr)
hs_free_database(db_);

db_ = db;

hs_error_t err_scratch = hs_alloc_scratch(db_, &scratch_);
if (err_scratch != HS_SUCCESS)
{
std::cout << "ERROR: hs_alloc_scratch failed." << std::endl;
return false;
}

return true;
}

// 인자로 전달된 패턴 정보가 Hyperscan Pattern DB에 존재하는지 여부를 반환하는 함수입니다.
bool scanPattern(std::string pattern)
{
unsigned int match_count = 0;

// 매칭되는 패턴이 Hyperscan Pattern DB에 존재한다면 hs_scan 함수의 6번째 인자로 등록되는
// 콜백 함수(해당 코드에서는 람다 함수로 구현)를 통해 결과를 확인할 수 있으며,
// 5번째 인자인 ctx를 통해 hs_scan 외부의 변수에 콜백 함수의 결과를 저장할 수도 있습니다.
if (hs_scan(
db_,
pattern.c_str(),
pattern.length(),
0,
scratch_,
[](unsigned int id, unsigned long long from, unsigned long long to, unsigned int flags, void *ctx) -> int
{
size_t *matches = (size_t *)ctx;
(*matches)++;
std::cout << "id: " << id << " from: " << from << " to: " << to << " flags: " << flags << std::endl;
return 0;
},
&match_count) != HS_SUCCESS)
{
std::cout << "hs_scan failed." << std::endl;
return false;
}

std::cout << "match count: " << match_count << std::endl;
return true;
}

private:
hs_database_t *db_;
hs_stream_t *stream_;
hs_scratch_t *scratch_;

std::vector<unsigned int> ids_;
std::vector<std::string> patterns_;
std::vector<unsigned int> flags_;

// parseFile 함수에서 호출하며 파일에 등록된 flags 정보를 Hyperscan에서 정의한 상수 값으로 변환하는 작업을 합니다.
int parseFlags(const std::string &flagsStr)
{
unsigned int flags = 0;

for (const auto &c : flagsStr)
{
switch (c)
{
case 'i':
flags |= HS_FLAG_CASELESS;
break;
case 'm':
flags |= HS_FLAG_MULTILINE;
break;
case 's':
flags |= HS_FLAG_DOTALL;
break;
case 'H':
flags |= HS_FLAG_SINGLEMATCH;
break;
case 'V':
flags |= HS_FLAG_ALLOWEMPTY;
break;
case '8':
flags |= HS_FLAG_UTF8;
break;
case 'W':
flags |= HS_FLAG_UCP;
break;
case '\r': // stray carriage-return
break;
}
}

return flags;
}
};

int main()
{
HyperscanTest hst;

// 1. 정규표현식 샘플 패턴들이 등록된 파일을 Hyperscan에서 사용 가능한 형태로 파싱
if(!hst.parseFile("./pattern.db"))
{
std::cout << "parseFile failed." << std::endl;
return 0;
}

// 2. Hyperscan Pattern DB 컴파일
if(!hst.compileDatabase())
{
std::cout << "compileDatabase failed." << std::endl;
return 0;
}

do
{
std::string pattren = "";
std::cout << "체크할 패턴을 입력하세요 >> ";
getline(std::cin, pattren);

// 3. 컴파일된 Hyperscan Pattern DB를 기반으로 패턴 체크
if(!hst.scanPattern(pattren))
{
std::cout << "scanPattern failed." << std::endl;
}
} while (true);

return 0;
}

정규표현식 샘플 패턴 등록

아래 예시는 (순서대로) 이메일, 전화번호, 휴대폰번호, 우편번호, 주민등록번호에 대한 정규표현식 샘플 패턴이며, 생성한 pattern.db는 테스트코드 빌드 결과 생성되는 실행 파일(example)과 동일한 경로에 위치시키면 됩니다.

1
2
3
4
5
6
7
$ vim pattern.db

1:/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/
2:/^(070|02|031|032|033|041|042|043|051|052|053|054|055|061|062|063|064)-\d{3,4}-\d{4}$/
3:/^(010|011|016|017|018|019)-\d{3,4}-\d{4}$/
4:/^\d{3}-?\d{3}$/
5:/^\d{2}[0-1]\d[0-3]\d-?[1-6]\d{6}$/

빌드 및 테스트

1
2
3
4
5
6
7
8
9
10
11
12
$ c++ -I /usr/local/include/hs example.cpp -o example -L <hyperscan-source-path>/build/lib -lhs
$ ./example
체크할 패턴을 입력하세요 >> 900101-1234567
id: 5 from: 0 to: 14 flags: 0
match count: 1
체크할 패턴을 입력하세요 >> 010-1234-5678
id: 3 from: 0 to: 13 flags: 0
match count: 1
체크할 패턴을 입력하세요 >> bluetomorrow90@gmail.com
id: 1 from: 0 to: 24 flags: 0
match count: 1
체크할 패턴을 입력하세요 >>

example 실행 시 libhs.so를 참조하지 못하는 경우에는 아래 명령어를 통해 라이브러리 참조 경로를 직접 지정해줍니다.

1
$ export LD_LIBRARY_PATH=<hyperscan-source-path>/build/lib

마치며

지금까지 Hyperscan 소스 빌드 과정과 정규표현식 샘플 패턴을 체크하기 위한 테스트 코드 작성에 대해 알아보았습니다. 해당 포스팅에서는 Hyperscan API의 가장 기본적인 동작들만을 살펴보았구요, Hyperscan 공식 문서를 살펴보면 멀티쓰레드 환경(“main” thread에서 pattern db를 compile하고 2개 이상의 “worker” thread에서 scan을 수행)에서도 사용이 가능하다고 하니 어떤식으로 구현해야할지 함께 고민해보면 좋을 것 같습니다. 긴 글 읽어주셔서 감사합니다🙂

In a scenario where a set of expressions are compiled by a single “main” thread and data will be scanned by multiple “worker” threads, the convenience function hs_clone_scratch() allows multiple copies of an existing scratch space to be made for each thread (rather than forcing the caller to pass all the compiled databases through hs_alloc_scratch() multiple times).



해당 게시글에서 발생한 오탈자나 잘못된 내용에 대한 정정 댓글 격하게 환영합니다😎

Reference