MongoDB 仓库中的 RE2 正则引擎:线性时间匹配、C++ API 与 Bazel 构建全解析
【免费下载链接】mongoThe MongoDB Database项目地址: https://gitcode.com/GitHub_Trending/mo/mongo
RE2 是一款以安全性为首要目标的正则表达式库,自 2006 年起在 Google 及众多公司生产环境中服役,其核心承诺是"匹配时间与输入长度成线性关系",即使面对不可信用户构造的恶意正则也不会发生灾难性回溯。本文以 MongoDB 仓库中随源码分发的 RE2(位于 src/third_party/re2/dist/README.md)为主体,完整讲解其设计哲学、语法边界、C++ 匹配接口、子匹配提取、预编译对象、Options 配置、Unicode 语义、多引擎实现原理与三种构建方式,并对照仓库内的 BUILD.bazel、re2.h 与测试源码给出源码级佐证,帮助读者在 MongoDB 工程体系内正确编译、链接与使用 RE2。
一、设计哲学:为什么说"安全是 RE2 的第一目标"
RE2 的诞生背景是:传统回溯型正则引擎(如 PCRE/Perl)在处理(a|a)*b这类模式与恶意输入组合时,匹配耗时可能随输入长度指数级增长(即灾难性回溯,Catastrophic Backtracking),从而成为 DoS 攻击面。RE2 从根上规避了这个问题,其保证可以归纳为三条:
- 线性时间保证:匹配时间与输入字符串长度渐近线性相关,不受正则表达式结构的影响;
- 受控内存预算:解析器、编译器和执行引擎都在一个可配置的内存预算内工作,预算耗尽时优雅失败(fail gracefully),而不是耗尽进程内存;
- 杜绝递归:实现全程避免递归,从根本上防止栈溢出。
README 用一句非常精辟的对比总结了 RE2 与回溯引擎的气质差异:
RE2 是"悲观"的,回溯引擎是"乐观"的。回溯引擎逐个尝试每个备选分支,当第一个分支经常命中时会很快;而 RE2并行评估所有分支,避免了"最后一个分支才命中"时的性能惩罚,代价是固定开销。正是这种悲观,成就了 RE2 的安全。
同时 README 也坦诚声明了两个非目标:
- 并非在所有场景下都比其他引擎快——更复杂的表达式会带来更大的常数因子,更长的表达式会提高安全处理所需的开销;
- 并非实现 Perl/PCRE 的全部特性——凡已知只有回溯方案才能实现的构造一律不支持,因此反向引用(backreferences)和环视断言(look-around)被明确排除(子匹配提取仍然支持,见下文)。
仓库中的证据
RE2 以第三方库形式随 MongoDB 仓库源码分发,目录为src/third_party/re2/dist/,包含LICENSE、CONTRIBUTING.md、SECURITY.md、README.md、MODULE.bazel(Bazel Bzlmod 模块描述)以及re2/、util/两个源码子目录。其多引擎实现文件分布在 re2/ 下,包括nfa.cc(NFA 执行)、dfa.cc(DFA 执行)、onepass.cc(OnePass 专用路径)、bitstate.cc(位状态模拟)、parse.cc(语法解析)、compile.cc(程序编译)、prog.cc/h(编译产物 Prog)、simplify.cc(正则化简)等,这些正是"线性时间、无递归"承诺的具体承载者,我们将在"多引擎架构"一节展开。
二、语法支持:POSIX 模式与 Perl 模式
RE2 支持两套语法模式,默认是 Perl 模式:
| 模式 | 语法来源 | 匹配语义 |
|---|---|---|
| Perl 模式(默认) | 大多数 Perl 操作符 | 与 Perl 选择相同的匹配结果 |
| POSIX 模式 | 标准 POSIX(egrep)语法 | 最左最长(leftmost-longest)匹配 |
被排除的只有那些需要回溯才能实现、因而可能带来指数级运行时间的构造,典型就是反向引用与广义断言(generalized assertions)。re2.h开头的注释进一步补充了语法速览,例如\w(单词字符)、\d(数字)、\s(空白)、\b(词边界)、(?i)(忽略大小写)、.*?(最小匹配)等都是支持的;同时明确\Z这类断言也不可用。
在 C++ 字符串字面量中写正则需要双重转义(如"(\\w+):(\\d+)"),而使用 C++11 原始字符串字面量则不需要:
R"(hello (\w+) world)" // \w 匹配单词字符 R"(version (\d+))" // \d 匹配数字 R"((?i)hello)" // (?i) 开启忽略大小写 R"(/\*(.*?)\*/)" // .*? 最小匹配从源码看,语法解析集中在 parse.cc,它还配套了 mimics_pcre.cc,该文件用于判断某个正则是否"行为上与 PCRE 相同"(对应仓库中的mimics_pcre_test),这从侧面印证了 RE2 在语法子集上对 PCRE 的刻意对齐与差异化管理。
三、C++ 匹配接口:FullMatch 与 PartialMatch
RE2 的原生语言是 C++,核心只有两个基本操作符:
RE2::FullMatch:要求正则与整个输入文本完全匹配;RE2::PartialMatch:在输入文本的子串中寻找匹配;POSIX 模式下返回最左最长匹配,Perl 模式下返回与 Perl 相同选择的结果。
README 给出的最小示例:
assert(RE2::FullMatch("hello", "h.*o")) assert(!RE2::FullMatch("hello", "e")) assert(RE2::PartialMatch("hello", "h.*o")) assert(RE2::PartialMatch("hello", "e"))注意FullMatch("hello", "e")失败,而PartialMatch("hello", "e")成功——这正是两个操作符语义差异的直观体现。re2.h中还给出了"提取第一个数字"的经典用例:
int number; ABSL_CHECK(RE2::PartialMatch("x*100 + 20", "(\\d+)", &number)); ABSL_CHECK_EQ(number, 100);错误码枚举
re2.h 定义了完整的ErrorCode枚举,编译失败后可通过re.error()获取,常见值包括:
ErrorBadEscape:非法转义序列ErrorBadCharClass/ErrorBadCharRange:字符类或其区间非法ErrorMissingBracket/ErrorMissingParen:缺少闭合的]或)ErrorUnexpectedParen:多余的闭合)ErrorTrailingBackslash:正则末尾孤立的\ErrorRepeatArgument/ErrorRepeatSize/ErrorRepeatOp:重复操作符参数缺失、参数非法或操作符非法ErrorBadPerlOp:非法 Perl 操作符ErrorBadUTF8:正则中含非法 UTF-8ErrorBadNamedCapture:命名捕获组非法ErrorPatternTooLarge:模式过大,编译失败
四、子匹配提取:string / 整数 / string_view
两个匹配函数都接受额外的输出参数用于存放子匹配(submatch),参数类型可以是string*、整数类型指针,或absl::string_view*。README 特别解释:absl::string_view与std::string_view非常相似,只是出于历史原因 RE2 使用前者;它是一个"指向原始输入文本的指针 + 长度",行为像字符串但不持有自己的存储,因此一旦原始文本被删除或超出作用域,就不能再使用这个 view——与裸指针的注意事项一致。
README 给出的完整示例(保留全部细节):
// 解析成功。 int i; string s; assert(RE2::FullMatch("ruby:1234", "(\\w+):(\\d+)", &s, &i)); assert(s == "ruby"); assert(i == 1234); // 失败:"ruby" 无法被解析为整数。 assert(!RE2::FullMatch("ruby", "(.+)", &i)); // 成功;不提取数字。 assert(RE2::FullMatch("ruby:1234", "(\\w+):(\\d+)", &s)); // 成功;跳过 NULL 参数。 assert(RE2::FullMatch("ruby:1234", "(\\w+):(\\d+)", (void*)NULL, &i)); // 失败:整数溢出导致 i 中不保存值。 assert(!RE2::FullMatch("ruby:123456789123", "(\\w+):(\\d+)", &s, &i));re2.h对提取语义补充了三条重要细节:
- 失败时不改动:匹配失败时,任何输出对象都不会被修改;
- 转换失败即中止:匹配成功后按顺序把各子匹配转换并赋给输出对象,直到某个转换失败为止;对
string/string_view这类不检查内容的类型,转换不会失败,因此常见情况下失败原因就是"匹配失败"; - 整数溢出算失败:目标文本无法被解析为对应整数类型(如超出范围)时,
FullMatch/PartialMatch返回false——这正是上面最后一个断言的设计意图。
另外,re2.h注释给出了一条性能经验:请求子匹配会让成功匹配明显变慢(目前甚至慢于 PCRE),但失败匹配与不提取子匹配的匹配则非常快——这与 RE2"并行评估所有分支、快速否决"的悲观设计一脉相承。
运行时参数个数:FullMatchN 与 Arg
当参数个数在运行时才能确定(例如正则本身是动态计算的)时,可以改用N系列操作:
const RE2::Arg* args[10]; int n; // ... 用 RE2::Arg 对象的指针填充 args ... // ... 将 n 设为 RE2::Arg 对象的个数 ... bool match = RE2::FullMatchN(input, pattern, args, n);上面的调用等价于RE2::FullMatch(input, pattern, *args[0], ..., *args[n-1])。RE2::Arg类在re2.h中定义,是"把用户传入的指针包装成特殊 Arg 对象"的机制。
进制解析:Hex / Octal / CRadix
默认情况下,传入数值指针时对应文本按十进制解析。通过RE2::Hex()、RE2::Octal()、RE2::CRadix()包装指针可以改变进制,其中CRadix按 C 风格识别0x(十六进制)与0前缀(八进制),无前缀则回退到十进制:
int a, b, c, d; ABSL_CHECK(RE2::FullMatch("100 40 0100 0x40", "(.*) (.*) (.*) (.*)", RE2::Octal(&a), RE2::Hex(&b), RE2::CRadix(&c), RE2::CRadix(&d)); // 结果:a == b == c == d == 64五、预编译正则对象:RE2 re("...")
前面所有示例在每次调用时都重新编译正则。高频路径下应把编译结果缓存为RE2对象,编译一次、复用多次:
RE2 re("(\\w+):(\\d+)"); assert(re.ok()); // 编译成功;若失败,查看 re.error() assert(RE2::FullMatch("ruby:1234", re, &s, &i)); assert(RE2::FullMatch("ruby:1234", re, &s)); assert(RE2::FullMatch("ruby:1234", re, (void*)NULL, &i)); assert(!RE2::FullMatch("ruby:123456789123", re, &s, &i));关键事实(来自 re2.h):
RE2对象即"预编译的正则表达式",对应一个内部Prog程序(见下文"编译流水线");RE2对象可被多个线程安全地并发使用——这在多线程服务端场景(如 MongoDB 这类数据库的查询/校验路径)中是重要的可用性保证;re.ok()判断编译是否成功,re.error()返回失败详情(ErrorCode枚举 + 可读文本)。
六、Options:Quiet、Latin1、POSIX 与自定义选项
构造函数接受可选的第二个参数用于覆盖默认选项。三个最常用的预置选项:
| 预置选项 | 作用 |
|---|---|
RE2::Quiet | 静默"正则解析失败"时通常打印到 stderr 的错误信息 |
RE2::Latin1 | 禁用 UTF-8,按 Latin-1 解释模式与输入 |
RE2::POSIX | 使用 POSIX 语法与最左最长匹配 |
Quiet的典型用法——解析非法模式而不污染 stderr:
RE2 re("(ab", RE2::Quiet); // 解析失败时不要写 stderr assert(!re.ok()); // 可通过 re.error() 查看细节完整的可配置项(大小写折叠、最大内存、最长匹配、日志级别、encoding、never_nl、dot_nl、one_line、longest_match等)定义在RE2::Options类中,位于 re2.h 的class Options。README 明确建议:可以自行声明一个RE2::Options对象并按需配置,例如:
RE2::Options opts; opts.set_encoding(RE2::Options::EncodingLatin1); // 或 EncodingUTF8 opts.set_log_errors(false); // 等价于 Quiet RE2 re(pattern, opts);(上述set_encoding/set_log_errors为Options提供的典型 setter,完整清单以 re2.h 为准。)
七、Unicode 处理:基于码点,不做归一化
RE2 在Unicode 码点(code point)层面工作,不进行任何归一化(normalization)。README 给出的例子非常典型:
- 正则
/ü/(U+00FC,带分音符的 u)不能匹配输入"ü"(U+0075 U+0308,u 后跟组合分音符)。
因为前者是单个预组合码点,后者是两个码点序列,二者在码点层面不相等。归一化本身是个庞大复杂的主题(涉及 NFC/NFD 等),RE2 的立场是不越俎代庖。README 给出的实用建议是:如果确实需要这类匹配,请在预处理阶段把"正则表达式"和"输入文本"都归一化后再交给 RE2。
re2.h还补充了一条与 UTF-8 相关的语义:使用 UTF-8 编码时,忽略大小写匹配执行的是简单大小写折叠(simple case folding),而非完整折叠(full case folding)——两者在个别语言字符(如涉及多字符映射的情况)上行为不同,需要精确语义时应查阅 Unicode 标准(UAX #15 等资料)。
八、增量扫描:Consume 与 FindAndConsume
除了FullMatch/PartialMatch,re2.h还记录了两种适合"流式/增量"解析的操作:
RE2::Consume:把匹配锚定在字符串开头,匹配成功后推进absl::string_view越过已匹配文本,适合逐行解析var = value这类固定格式:
std::string contents = ...; // 填充字符串 absl::string_view input(contents); // 用 string_view 包一层 std::string var; int value; while (RE2::Consume(&input, "(\\w+) = (\\d+)\n", &var, &value)) { // ...处理 var/value... }注意:若正则可能匹配空串,input将不前进(推进 0 字节),循环体必须检查该情况并手动前进或跳出,否则会死循环。
RE2::FindAndConsume:与Consume类似但不把匹配锚定在开头,例如反复提取字符串中的所有单词:
RE2::FindAndConsume(&input, "(\\w+)", &word)九、构建与安装:make / CMake / Bazel 三条路径
RE2 支持 GNU make、CMake、Bazel 三种构建方式。构建 RE2 本身需要 C++17 编译器与 Abseil 库;构建测试和基准还需要 GoogleTest 与 Google Benchmark。
9.1 GNU make 方式(最简)
make make test make benchmark make install make testinstall9.2 依赖获取
- Linux:
apt install libabsl-dev libgtest-dev libbenchmark-dev - macOS:
brew install abseil googletest google-benchmark pkg-config-wrapper - Windows:
vcpkg install abseil gtest benchmark或vcpkg add port abseil gtest benchmark
9.3 CMake 方式
如果标准 Makefile 在查找依赖时遇到问题,切换到 CMake 往往能解决:
rm -rf build cmake -DRE2_TEST=ON -DRE2_BENCHMARK=ON -S . -B build cd build make make test make installCMake 相关的几个要点:
- 启用 benchmark 时,
make test会构建并运行测试二进制,同时构建regexp_benchmark二进制但不运行它; - 不需要测试/基准时,省略对应
-D参数即可,此时也不需要 GoogleTest/Benchmark 依赖; -DRE2_USE_ICU=ON会引入 ICU Unicode 库依赖,同时扩展\p与\P模式可用的属性名列表(如更多 Unicode 属性类别);- CMake 还能生成 Visual Studio、Xcode 工程以及 Cygwin、MinGW、MSYS makefile。Visual Studio 用户需要 2019 或更高版本;Cygwin 用户必须从 Cygwin 命令行(而非 Windows 命令行)运行 CMake。
作为依赖集成进自有 CMake 工程时,有两种方式:add_subdirectory()(依赖的源码位于你工程的子目录中)与find_package()(依赖的二进制已构建并安装到系统)。两种方式下target_link_libraries(... re2::re2)都应"开箱即用"。
9.4 Bazel 方式(MongoDB 仓库的集成形态)
独立使用 Bazel 构建 RE2 时,Bazel 会自动处理依赖(仍需自行下载 Bazel,可通过 Bazelisk 管理版本);在仓库内执行:
bazelisk build :all bazelisk test :all在 MongoDB 仓库中,RE2 已经通过 Bazel 完整接入,见 src/third_party/re2/dist/BUILD.bazel。该文件的核心是一个cc_library(name = "re2")目标,从中可以读出非常具体的集成信息:
- 源码清单:包含
re2/re2.cc、re2/compile.cc、re2/dfa.cc、re2/nfa.cc、re2/onepass.cc、re2/bitstate.cc、re2/parse.cc、re2/prog.cc、re2/regexp.cc、re2/simplify.cc、re2/filtered_re2.cc、re2/set.cc、re2/mimics_pcre.cc、re2/tostring.cc、re2/unicode_casefold.cc、re2/unicode_groups.cc、re2/perl_groups.cc以及util/rune.cc、util/strutil.cc等; - 公共头文件:
re2/filtered_re2.h、re2/re2.h、re2/set.h、re2/stringpiece.h; - 依赖:一组 Abseil 目标(
@abseil-cpp//absl/...,包括strings、hash、container:flat_hash_map/set、container:inlined_vector、container:fixed_array、synchronization、types:span、types:optional、log、base等)——与 README"构建需要 Abseil"的说明完全对应; - 平台差异:
copts/linkopts通过select按平台调整-pthread——macOS 与 WebAssembly(wasm32/wasm64/emscripten/wasi)以及 Windows 不传-pthread,其余平台默认加上,注释明确指出"WebAssembly 的线程支持在每一层都很棘手"; - 可见性:
visibility = ["//visibility:public"],即仓库内其他 Bazel 目标可以公开依赖该库。
依赖方工程若通过 Bazel 使用 RE2,需保证编译标准不低于C++17(README 明确要求,并指向.bazelrc作为示例)。
十、测试体系:从单元测试到穷举测试
BUILD.bazel中暴露了完整且分层清晰的测试矩阵,全部位于 re2/testing/:
- 小规模单元测试(
size = "small"):charclass_test(字符类)、compile_test(编译)、parse_test(语法解析)、simplify_test(正则化简)、regexp_test、re2_test(核心 API)、re2_arg_test(Arg 参数机制)、search_test、set_test(批量匹配 Set)、filtered_re2_test(预过滤)、possible_match_test(可能的匹配前缀)、required_prefix_test(必要前缀)、mimics_pcre_test(与 PCRE 行为对齐度)、string_generator_test(测试用字符串生成器); - 大规模测试(
size = "large"):dfa_test(DFA 引擎专项)、exhaustive_test及exhaustive1/2/3_test(穷举测试:对生成的正则与字符串集合做全量比对,这是验证"与参考语义一致"的关键手段)、random_test(随机模糊测试); - 基准:
regexp_benchmark(testonly = 1的cc_binary,依赖@google_benchmark)。
这套"单元 + 穷举 + 随机 + 基准"的测试结构,正是 RE2 敢在安全敏感场景(不可信正则 + 不可信输入)下承诺线性时间的重要底气。另有一个testing库(testonly = 1)把backtrack.cc(参考回溯实现,用于对照验证)、exhaustive_tester.cc、regexp_generator.cc、string_generator.cc与util/pcre.cc组织起来,作为"参照实现"支撑穷举测试。
十一、核心实现原理:编译流水线与多引擎架构
结合 re2/ 目录源码,可以把 RE2 的工作方式拆成"编译期"与"执行期"两段:
编译流水线(源码级)
parse.cc:把正则字符串解析为 AST(Regexp对象树,定义于regexp.h),同时执行语法校验并生成上文提到的ErrorCode;simplify.cc:对Regexp做等价化简(如折叠嵌套、规并字符类、re2内部 normalization),缩小后续编译规模;compile.cc:把化简后的Regexp编译成指令形式的程序Prog(prog.h/prog.cc),这是 NFA/DFA 等执行引擎统一消费的中间表示;- 配套的
unicode_casefold.cc/unicode_groups.cc/perl_groups.cc是由make_unicode_casefold.py、make_unicode_groups.py、make_perl_groups.pl等脚本生成的 Unicode 数据表,支撑(?i)折叠、\p{...}属性与 Perl 字符类。
多引擎执行(执行期)
RE2 内部按输入与正则特征在多个执行器间选择,对应文件:nfa.cc(NFA 模拟)、dfa.cc(DFA 构造与缓存)、onepass.cc(OnePass:无分支/少分支正则的专用快速路径)、bitstate.cc(位向量状态集模拟)。这种"多引擎 + 自动选择"的架构,配合前面提到的内存预算(预算耗尽时优雅降级/失败)与无递归约束,共同保证了最坏情况下的线性时间与可控资源占用。pod_array.h、sparse_array.h、sparse_set.h、bitmap256.cc等数据结构文件则是这些引擎高效运行的基础设施。
批量与预过滤:Set 与 FilteredRE2
仓库还提供了两类面向规模化场景的 API:
RE2::Set(set.cc/set.h):把大量正则一次性编译成一个集合,对一段输入同时测试"匹配了集合中的哪些正则",避免逐个编译、逐个匹配的开销;FilteredRE2(filtered_re2.cc/filtered_re2.h):在真正执行匹配前,先对所有正则做必要前缀/可能匹配前缀的预计算(prefilter.cc、prefilter_tree.cc),先用廉价测试快速排除绝大多数不可能命中的正则,再对候选集做精确匹配——适合"数千条规则对一段文本做路由/分类"的服务端场景。
十二、多语言生态:官方封装与同源移植
RE2 原生实现是 C++,但生态覆盖广泛:
- 官方 Python 封装位于 RE2 仓库的
python/目录,发布在 PyPI 上名为google-re2。README 特别提醒:PyPI 上还有一个re2包,但那并非 RE2 作者发布、且已不再维护,请务必使用google-re2; - 其他语言的非官方封装包括:C(
cre2)、D(re2d)、Erlang、Inferno、Node.js(npm 上的re2)、OCaml(Jane Street 维护)、Perl(re::engine::RE2,CPAN)、R、Ruby、WebAssembly(re2-wasm)等; - 同源移植:
RE2J是把 RE2 C++ 代码移植为纯 Java 的版本,RE2JS是 RE2J 到 JavaScript 的移植; - 同原理不同代码:Go 的
regexp标准包与 Rust 的regexcrate 与 RE2不共享代码,但遵循相同原则、接受相同语法、提供相同的效率保证——也就是说,即使在其他语言栈中,也可以用同一套"线性时间、无灾难性回溯"的思维模型。
十三、在本仓库中的定位与使用建议
MongoDB 将 RE2 以随源码分发的第三方库形式放在src/third_party/re2/dist/,通过 Bazel 的cc_library(name = "re2")目标(BUILD.bazel)对外暴露,仓库内其他目标可以直接依赖使用。需要指出的是,MongoDB 服务端自身的通用正则表达式功能(如$regex查询)走的是PCRE2路线——src/mongo/util/pcre.h 明确说明它是"为 PCRE2 库封装的 MongoDB C++ 包装层",且刻意通过封装隔离了对 pcre2 头文件的直接依赖。因此在本仓库中,RE2 是作为独立、可选、安全优先的正则基础设施随仓库提供的:
- 如果你的组件要处理不可信输入上的正则匹配(如用户提供的模式做匹配/过滤/校验),且能接受 RE2 的语法子集(不支持反向引用与环视),RE2 的线性时间保证是最合适的选择;
- 如果需要 Perl/PCRE 全量语法(反向引用、环视等),则应使用服务端已有的 PCRE2 封装
mongo::pcre; - 无论选择哪条路径,本仓库的 Bazel 目标已经就绪,构建时只需正确声明依赖并保证 C++17 编译标准即可。
结语
从设计目标到多引擎实现,从FullMatch/PartialMatch到Consume/FindAndConsume,从 make/CMake 到仓库内现成的 Bazel 目标,RE2 用"悲观但安全"的方式解决了正则引擎最棘手的可用性问题。对照 README.md 与 re2.h、BUILD.bazel 及 re2/testing 的测试源码,读者既可以在 MongoDB 的 Bazel 工程内直接复用这一安全正则基础设施,也可以把这套"线性时间 + 内存预算 + 无递归"的工程方法论迁移到自己的系统中。
【免费下载链接】mongoThe MongoDB Database项目地址: https://gitcode.com/GitHub_Trending/mo/mongo
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考