re2 1.1.1
re2: ^1.1.1 copied to clipboard
Linear-time regular expressions for Dart via Google's RE2 over FFI. Immune to catastrophic backtracking (ReDoS).
1.1.1 #
- The README opens with a recording of the package running, rendered from a
real execution rather than drawn: the command is run, its exit code checked,
and the frames built from the bytes it printed.
tools/term-trailer.shin the portfolio repository makes it, so it can be regenerated when the numbers move.
1.1.0 #
- The README now answers, in its first screen, why to reach for this rather than the zero-dependency route or the package that already owns the category. Both answers carry the file and line, or the issue number, that a reader can check. A "reach for it when" list and a sentence on when to skip it follow, because a page that only argues for itself is not useful for deciding.
1.0.5 #
- The README opens on the curve now.
tool/redos_chart.darttimes both engines on(a+)+$as it runs and drawsdoc/redos.pngfrom what it measured. The picture carries this machine's numbers rather than a claim. On the run that produced the committed file,dart:corewent from 6 ms at 18 characters to 2.9 s at 28, doubling every two characters, while re2 sat near 27 microseconds across the same range and finished 100,000 characters in 5.1 ms. - The caption names the dip at the left of the re2 line rather than smoothing
it away. That point is the first
hasMatchin the process paying for the native call once, and every point after it is the steady cost. - Documentation and tooling only. No API, behaviour or native change.
1.0.4 #
Documentation only. No API, behaviour or native change.
- The platform table said Flutter mobile was not tested yet. It is tested now,
and it works. A Flutter app that constructs a
Re2at startup and prints the result reportshasMatch=trueon an iPhone 17 Pro simulator running iOS 26.5 and on an Android 15 arm64 emulator (API 35). The row reads yes. - The Platforms section says how that was checked. Running the match inside the
app process is the evidence that matters here, because a build can go green
while the library fails to load: that is what 1.0.1 shipped on Android, where
the first
Re2(...)threwdlopen failed.
1.0.3 #
Documentation only. The README quoted several measurements more precisely than anything in the repository could reproduce, and one claim had no committed artifact behind it at all.
- Quote the FFI overhead as a measured band rather than one figure. The README
read "193 microseconds for each of the three patterns". Three runs of
bench/ffi_overhead.darton the 64 KB row produced 186 to 191, and no run produced 193 on all three at once. It now reads about 190 and gives the spread. TheRegExprange in the same sentence read 7 to 275; measured, it is under 7 to about 270. - Say that the ratio table is a single run. Its cells move by a few percent between runs. Across three runs the smallest literal-alternation cell at the four sizes the table shows is 20.17x, at 16 B, where both engines print near the resolution of the figure. The sentence reading off that column now says over 20x where it used to say at least 22x.
- Correct the compiled-build row and drop an unmeasured claim about Flutter.
The 64 KB row quoted from
dart build cliis one printed row rather than three figures gathered from different runs: 0.20x, 0.35x and 15.70x.RegExpslows by 2.6x to 5.3x against re2's 1.5x to 1.8x. The README also said a Flutter release build made that row the relevant one for anyone shipping a Flutter app. It is a different toolchain and nothing here has measured it. The README now says to run the benchmark in the mode you ship. - Add
test/syntax_table_test.dart. The "Supported syntax" table said every row had been checked by constructing the pattern on both engines, and no committed code did that, which left the table free to go stale if either engine changed. Fourteen cases now do it: each pattern is constructed ondart:coreand onre2, run wherever both accept it, and the one difference that does not throw is covered as well, a defaultRegExp(r'\p{L}+')matching the literal textp{L}wherere2matches letters.
No API or behaviour change.
1.0.2 #
-
Fix the Android build loading nothing on a device. The shim is C++, and the toolchain links the C++ standard library dynamically there by default, so it shipped with a
DT_NEEDEDonlibc++_shared.sothat nothing puts in the APK. The build stayed green and the firstRe2(...)threwdlopen failed: library "libc++_shared.so" not found. It is linked statically now.Fixing that uncovered a second one underneath: RE2's DFA budgeting calls
log, Android keeps math in a separate libm, and its linker will not resolve a symbol from a library absent fromDT_NEEDED. While the shared C++ runtime was there it pulled libm in transitively and hid the omission; with static linking the device failed oncannot locate symbol "log". libm is now linked explicitly on Android and Linux.Verified end to end rather than by inspection: on an arm64 Android 15 image with SELinux enforcing, and on an iOS 26 simulator, a compile-and-match round trip now succeeds.
DT_NEEDEDreads libm, libdl, libc.
1.0.1 #
- Correct the 1,000,000-character figure in the README. It read 1.9
milliseconds, and no committed artifact produced that number:
bench/bench.dartstopped at 100,000 characters, and the 1,000,000-character test asserts a time bound without printing a time. The benchmark now runs the 1,000,000-character case as its last ReDoS row, and the README quotes what it prints, about 6 milliseconds. No API or behaviour change. (That row is a single shot and lands between 5.2 and 6.5 milliseconds across runs here, which is why it is quoted to the nearest millisecond rather than the 5.9 this entry first claimed.) - Correct the
re2figure for the 28-character case in the README. It read 2 microseconds, and nothing measurable produced it. The rowbench/bench.dartprinted is the firsthasMatchin the process, which costs a few hundred microseconds because it pays a one-time warm-up; a call in steady state costs about 0.17 microseconds. The 2 came fromdoc/benchmark.pngand sat between the two, matching neither. The benchmark now prints both, labelledre2 (first call)andre2 (warm loop), the warm one measured with the same loop-and-divide the benign section already used, and the README quotes both and names the reason for the gap. The warm loop runs after the single-shot rows: run earlier it warms them, and the 100,000- and 1,000,000-character rows then measure something else. The warm-up is not the dynamic library, which is loaded, and the pattern compiled, by the untimed constructor. - Correct the
dart:corefigure in the same sentence. It read 2.75 seconds, which came from the chart and not from the benchmark;bench/bench.dartprints about 3 seconds for that row. - Remove
doc/benchmark.pngand its pub.dev screenshot entry. The chart drew seven values ofnand called every point a real median measurement, but no committed code produced any of them, and no such code has ever been in this repository. Its flat "2 us" line matched nothing measurable, its "1.3 million times faster" followed from that line, and its caption still carried the 1.9 milliseconds the first entry above retracts. A chart this repository cannot regenerate is the same defect as a number it cannot. It is gone rather than redrawn into figures that would drift again. What it showed is inbench/bench.dart, which anyone can run. - Quote the benign FFI overhead as a bit under 2x rather than roughly 2x. It measures 1.7x to 1.8x now that the ReDoS section warms the shared match path before the benign loop runs.
1.0.0 #
First stable release. From here the public API follows semantic versioning: a breaking change will not land without a major-version bump.
- Make
Re2Match's constructor private. It took the match's internal representation as positional arguments (parallel start and end lists plus a name-to-index map), so leaving it public would have frozen that representation into the API and let outside code build a match with arbitrary internal state. Nothing outside the package ever constructed one:Re2.firstMatchandRe2.allMatchesstill build everyRe2Match, and its methods and getters (group,namedGroup,groupNames,operator [],start,endand the rest) are unchanged. - Narrow
Re2Match.patternfromPatterntoRe2. The value there is always theRe2that produced the match, so callers reach its members without a cast. This mirrorsRegExpMatch, which narrows its ownpatterntoRegExp. Narrowing a getter's return type is a breaking change, which is why it is done now rather than after the freeze. - Correct an overstated parity claim in the docs. The README and the 0.3.0
changelog entry said results "match
dart:core'sRegExpexactly, including UTF-16 offsets outside the Basic Multilingual Plane", and theallMatchesdoc comment claimed the same for its results. The offset half holds: offsets are UTF-16 indices, astral characters count as two units, andsubstring(match.start, match.end)is always the matched text. The parity half was too strong. RE2 matches whole Unicode code points, the wayRegExp(unicode: true)does, so on non-BMP input a single-character construct like.matches a whole astral code point where a defaultRegExpmatches one UTF-16 code unit. On ASCII and BMP input the two agree. The README now states the difference, and theallMatchesdoc comment no longer claims exact parity.
0.5.2 #
- Rework the README around how RE2 actually works: a log-scale benchmark of
(a+)+$where the backtracking engine reaches 2.75 s at 28 characters while re2 stays near 2 microseconds, and a diagram of why (a backtracking engine searches every way to split the input, RE2 walks a state machine once). No code change.
0.5.1 #
- Fix a silent encoding bug: every FFI call site in
Re2andRe2Setturned a DartStringinto bytes withutf8.encode, which replaces an unpaired UTF-16 surrogate (a legal Dart string code unit on its own, for instanceString.fromCharCode(0xD800)) with the UTF-8 bytes for U+FFFD before RE2 ever saw the text. Two strings differing only in which lone surrogate they carried, or one with a lone surrogate against one with a literal U+FFFD, encoded to identical bytes and matched each other, andRe2.escapestopped being injective:Re2.escape(s1) == Re2.escape(s2)for two differents1/s2, andRe2(Re2.escape(s1)).hasMatch(s2)wastrue. Both are the kind of input that shows up in the untrusted-input case this package is for, such as a malformed\uD800-style JSON escape. - Encoding now goes through a small WTF-8 codec instead
(
lib/src/wtf8.dart): a lone surrogate gets its own 3-byte sequence rather than being substituted, and the encoding is byte-for-byte identical toutf8.encodefor every well-formed string. No public API change. - Fix a NUL-truncation bug in compile-error diagnostics:
re2_error()andre2_set_add()'s error output were read back as NUL-terminated C strings, but RE2's own diagnostic text can quote a slice of the original pattern and that slice can itself contain an embedded NUL (this package accepts patterns with embedded NULs). AFormatExceptionmessage could silently cut off mid-sentence. The native shim now also reports the exact byte length (re2_error_length(), and anerrLengthout-param onre2_set_add()), and the Dart side reads exactly that many bytes instead of scanning for a terminator. No public API change.
0.5.0 #
Re2Setmatches many patterns against one input in a single linear pass. Compile a list of patterns withRe2Set.compile([...])andmatches(input)returns the set of indices that fired, scanning the input once no matter how many patterns there are. This is the rule-engine shape, a firewall, a log classifier, a router, and it is exactly where a backtracking engine is worst: NRegExps mean N passes, each able to blow up, so the ReDoS exposure grows with the ruleset, while aRe2Setstays linear and pattern-count independent. Backed by RE2's ownRE2::Set.example/ruleset.dartruns a small WAF-style set and shows a request tripping two rules at once.
0.4.0 #
Re2.escape(String)turns an arbitrary string into a pattern that matches it literally, letting a search term or filename be interpolated into a larger pattern without its metacharacters being interpreted. Until now the only escaper wasdart:core'sRegExp.escape, which meant an untrusted fragment pulled the whole pattern back onto the backtracking engine, the one thing this package exists to avoid. Backed by RE2'sQuoteMeta.- The
Re2constructor takes an optionalmaxBytes, a cap on the memory the compiled pattern may use. A pattern from an untrusted source can be built to compile into a large program even though it matches in linear time; withmaxBytesit is rejected at construction with aFormatExceptioninstead of allocated. Null keeps RE2's own default (about 8 MB). Backed byRE2::Options::set_max_mem. - Together these close the untrusted-pattern half of the story; linear match
time already covered untrusted input. Verified:
escaperound-trips every sample and its output is inert as a pattern, cross-checked againstRegExp.escape; a pathological pattern under a smallmaxBytesthrows.
0.3.5 #
- Correct the README's platform claim. It said "Flutter support arrives when
build hooks land in stable Flutter", which is stale: build hooks are stable,
and re2 works in a Flutter app today. Verified end to end: it resolves,
compiles, and runs a match inside
flutter test, andflutter build macosproduces a working app that links the native library. The README now carries an honest support matrix, including that web is unsupported by design: adart:corefallback there would silently drop the linear-time guarantee the package exists to provide.
0.3.4 #
- Widen the native-toolchain constraints so the package can be installed in a
Flutter app at all.
hooks2.1.0 andnative_toolchain_c0.19.3 raised theirmetafloor to ^1.19.0, and Flutter's SDK pinsmetato 1.17.0, soflutter pub addfailed at version solving with "flutter from sdk is incompatible". Allowinghooks >=2.0.2andnative_toolchain_c >=0.19.2lets the solver pick a version that works with the pinnedmeta, while a pure-Dart project still resolves to the newest. No API or behaviour change.
0.3.3 #
- Shorten the screenshot description. pub.dev accepts up to 200 characters but scores only those under 160. The previous release published cleanly and quietly gave up the documentation points it was meant to earn.
0.3.2 #
- Declare the diagram in
pubspec.yamlso pub.dev renders it on the package page. It was already in the repository and the README, but pub.dev shows only what thescreenshots:field points at, leaving the page to open with prose where the picture should have been.
0.3.1 #
example/redos.dartruns the comparison the README asserts, on your machine, with both engines given the same pattern and the same input. On the classic(a+)+$a 29-character input takesdart:core2.77 s against re2's 30 us, and every two further characters multiply the left side by about four.- It also times
^(\w+\s?)*$, which is the kind of pattern written to validate a name or a list of tags rather than a contrived one, and which is no safer: 31 characters take 5.15 s. example/README.mdrecords something the usual advice gets wrong. Not every nested quantifier is exploitable: the widely copied email pattern stays fast on a long almost-matching address, because the literal dot between its loops fixes where each repetition ends. The danger is ambiguity, two loops that can claim the same characters, which is exactly what is hard to eyeball.
0.3.0 #
Re2now implementsPatternandRe2MatchimplementsMatch, so aRe2drops straight into theStringAPI in place of aRegExp:String.split,String.replaceAll,String.replaceAllMapped,String.contains,String.startsWith,String.splitMapJoinand the rest all accept it and run in RE2's guaranteed linear time. Results matchdart:core'sRegExpexactly, including UTF-16 offsets outside the Basic Multilingual Plane. AddsmatchAsPrefixto complete thePatterncontract. (Re2MatchimplementsMatchrather thanRegExpMatch, whosepatterngetter is typed asRegExp;namedGroupandgroupNamesremain available as methods.)
0.2.0 #
- Add
replaceAllandreplaceFirst, the linear-time counterparts toString.replaceAll(RegExp(...), ...). Because RE2 cannot backtrack, running a substitution over untrusted input or with a user-supplied pattern cannot hang the isolate. The rewrite string can reference capture groups with\1..\9.
0.1.0 #
Initial release, vendoring RE2 (last revision before the Abseil dependency).
Re2: compile a pattern once, thenhasMatch,firstMatch,stringMatch, andallMatches, withcaseSensitive,multiLine, anddotAllflags.Re2Match: positional and named group access.- Linear-time matching: catastrophic-backtracking patterns that hang
dart:coreRegExpstay linear here. - Backreferences and lookaround throw
FormatExceptionat construction, since RE2 does not support them. - Native code builds automatically via Dart build hooks (Dart 3.10+).