Ragel——字符串匹配神器

什么是Ragel

ragel是一个有限状态机编译器和解释器生成器,它支持生成c/c++、go、java等语言的代码。ragel一般用作文本解析和输入验证。

ragel的核心在于,它是一个确定性有限自动机,核心是由正则表达式运算符和动作嵌入运算符组成。通过动作嵌入运算符,可以改变状态机状态。

Ragel的优势

我们可以使用ragel做文本解析或输入验证。
当你需要解析复杂的HTTP请求,解析从HTTP请求拆分的字符串,再进行某些攻击检测时,传统的正则表达式总是显得有些力不从心。

举个简单的例子

当我们需要匹配a的n次方且b的n次方时,正则表达式无法表达所有状态

1
2
3
4
5
n=0,1,2...
使用正则表达式怎么写?
如果使用 a*b* 无法保证a的个数和b的个数一致

只有当n是确定且 n==4 时,正则表达式可以写成 a{4}b{4}

而ragel可以表示所有状态。下面我将使用ragel生成c代码,表示匹配a的n次方且b的n次方

cout.rl

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
#include <stdio.h>
#include <string.h>

%%{
machine foo;
action action_a {
a_cout ++;
}

action action_b {
b_cout ++;
}
main := 'a'* @action_a
'b'* @action_b
;
write data;
}%%

int main(int argc, char *argv[]) {
int cs;
size_t a_cout = 0;
size_t b_cout = 0;
if(argc > 1){
char *p = argv[1];
char *pe = p + strlen(p);
%%{
write init;
write exec;
}%%
}

printf("a_cout=%d\nb_cout=%d", a_cout,b_cout);
return 0;
}

Makefile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
all: compile
.PHONY: all

generate:
ragel -n -p -o cout.c cout.rl
.PHONY: generate

compile: generate
gcc cout.c
.PHONY: compile

clean:
rm -rf a.out
.PHONY: clean

可以看出,无论我们想输入什么样的形如aaabb字符,我们都可以拥有不同的策略来应对它,而不需要写多个正则表达式。

简单使用Ragel实现解析HTTP请求

下面是我用Ragel实现的一个解析HTTP协议的算法,写得非常简单,但足以看出ragel的解析能力。

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
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include <stdbool.h>

%%{
machine request;

Simple_Method =
"GET"
|
"POST"
|
"PUT"
|
"OPTION"
;
LF = '\n';
CR = '\r';
CRLF = CR? LF;
HTTP_VERSION = (
'HTTP/'
digit+
(
'.'
digit+
)?
);
url_path = [alnum'/']+
;
header =
alnum +': ' alnum+
;
key_value =
(alnum '=' alnum) ('&' alnum '=' alnum)?
;

main :=
Simple_Method ' /' url_path? ('?'key_value*)? ' ' HTTP_VERSION CRLF
(header CRLF)+ @{ success = true;}
;
}%%

%% write data;

int main(int argc,char * argv[]) {
if(argc > 1) {
int cs;
char *request;
FILE *file = fopen(argv[1],"rb");
if(file == NULL){
return 0;
}

fseek(file, 0, SEEK_END);
int length = ftell(file);
fseek(file, 0, SEEK_SET);

request = (char *)malloc(length + 1);
fread(request, 1, length, file);
fclose(file);

bool success = false;
char *p = request;
char *pe = request + length;
char *eof = pe;

%% write init;
%% write exec;

if (success) {
printf("HTTP 格式正确");
}
}

}

Ragel 强大的状态转移以及运算符

1
2
3
4
5
6
'>' 在匹配第一个字符成功后立即执行action,此时p指向匹配的第一个字符
'@' 尽可能长的完全匹配所有字符后执行action,此时p指向匹配的最后一个字符
'%' 完整匹配所有字符后执行action,此时p指向匹配的最后一个字符

注:关于'@'和'%'的使用可以自己做实验简单测试一下

总结

Ragel作为一个确定有限状态机,能在状态转移过程中插入动作,可随时更改转移状态,在文本解析或者协议解析方面有着非常大的优势。且可以生成c/c++、go、java代码任意调用,非常具有便携性。