Expand description
§Ferroni — Oniguruma’s regex engine in pure Rust
Bring your Oniguruma patterns to Rust. Ferroni combines look-behind,
backreferences and recursion with a familiar Regex API and a
multi-pattern Scanner for tokenizers and TextMate runtimes. Cargo handles
the build, including WebAssembly, without a C compiler or bindings.
The engine is a line-by-line port of Oniguruma for ASCII and UTF-8. It is feature-complete and maintained; the project status explains the scope. Start with the getting started guide or the Scanner guide for a runnable walkthrough.
§When to use Ferroni
Choose Ferroni when a pattern must behave as it does in Oniguruma: look-behind,
backreferences, atomic and possessive groups, subexpression calls, or a
TextMate grammar that highlights the way Shiki and VS Code do. Oniguruma’s
defaults differ from the regex crate for
anchors, capture groups, empty matches, group counts and byte input.
Coming from regex or onig
shows each difference in one call.
Ferroni is a backtracking engine, so it does not guarantee linear-time searches. Read Untrusted input before you search text or compile patterns that users supply.
§Regex
Regex compiles a pattern once and then searches as often as needed. The
crate root re-exports the prelude types, so use ferroni::Regex; works.
use ferroni::prelude::*; brings in all of them.
use ferroni::Regex;
let re = Regex::new(r"\d{4}-\d{2}-\d{2}").unwrap();
let m = re.find("Date: 2026-02-12").unwrap();
assert_eq!(m.as_str(), "2026-02-12");
assert_eq!(m.start(), 6);Regex::builder returns a RegexBuilder for options and syntax.
RegexBuilder::syntax_mode takes a typed Syntax:
use ferroni::{Regex, Syntax};
let re = Regex::builder("hello").case_insensitive(true).build().unwrap();
assert!(re.is_match("Hello World"));
let ruby = Regex::builder(r"\w+").syntax_mode(Syntax::Ruby).build().unwrap();
assert!(ruby.is_match("ok"));Regex::captures returns Captures, which indexes groups by number or
by name. Named groups come from the pattern itself:
use ferroni::Regex;
let re = Regex::new(r"(?<year>\d{4})-(?<month>\d{2})").unwrap();
let caps = re.captures("Released 2026-10.").unwrap();
assert_eq!(&caps[0], "2026-10");
assert_eq!(&caps["year"], "2026");
assert_eq!(caps.get(2).unwrap().as_str(), "10");Regex::find_iter yields the non-overlapping matches, and
Regex::captures_iter yields the groups of each one:
use ferroni::Regex;
let re = Regex::new(r"(\w+)=(\d+)").unwrap();
let pairs: Vec<(&str, &str)> = re
.captures_iter("a=1, b=22")
.map(|caps| (caps.get(1).unwrap().as_str(), caps.get(2).unwrap().as_str()))
.collect();
assert_eq!(pairs, [("a", "1"), ("b", "22")]);Regex::replace_all substitutes every match, with $1 or $name in the
template naming a group, and Regex::split yields the text between
matches:
use ferroni::Regex;
let re = Regex::new(r"(?<key>\w+)=(?<value>\d+)").unwrap();
assert_eq!(re.replace_all("a=1, b=22", "$value:$key"), "1:a, 22:b");
let sep = Regex::new(r",\s*").unwrap();
assert_eq!(sep.split("a, b,c").collect::<Vec<_>>(), ["a", "b", "c"]);Search from a byte offset with Regex::find_at, Regex::captures_at and
Regex::is_match_at. The text before the offset stays in view, so a
look-behind sees it. A slice of the text is a new text with no such context:
use ferroni::Regex;
let re = Regex::new(r"(?<=a)b").unwrap();
assert_eq!(re.find_at("ab", 1).unwrap().range(), 1..2);
assert!(re.find(&"ab"[1..]).is_none());§Scanner
A tokenizer asks one question many times: which of these patterns matches
next, at or after this position? Scanner compiles the whole set once and
answers it. It has the shape of vscode-oniguruma’s OnigScanner, which is
what TextMate grammars run on
(ADR-006).
use ferroni::{Scanner, ScannerFindOptions};
let mut scanner = Scanner::new(&[r"\bfn\b", r#""[^"]*""#, r"//.*$"]).unwrap();
let m = scanner.find_next_match(r#"fn f() "x""#, 0, ScannerFindOptions::NONE).unwrap();
assert_eq!(m.index, 0);
assert_eq!((m.captures()[0].start, m.captures()[0].end), (0, 2));The earliest match wins. When two patterns match at the same position, the one listed first wins:
use ferroni::{Scanner, ScannerFindOptions};
let mut scanner = Scanner::new(&["ab", "a"]).unwrap();
let m = scanner.find_next_match("xab", 0, ScannerFindOptions::NONE).unwrap();
assert_eq!((m.index, m.captures()[0].start), (0, 1));A ScannerMatch carries the winning pattern’s index and the byte spans of
its capture groups; index 0 is the whole match. To count UTF-16 code units,
as editors do, search an OnigString with
Scanner::find_next_match_utf16.
A grammar builds one scanner per rule context, and those scanners repeat the
same patterns. Build them with one ScannerPatternCache, which belongs to
the caller and is not process-wide. Each distinct pattern then compiles once:
use ferroni::{Scanner, ScannerConfig, ScannerPatternCache};
let config = ScannerConfig::default();
let mut cache = ScannerPatternCache::new();
let _top = Scanner::with_pattern_cache(&[r"\bfn\b", r#""[^"]*""#], &config, &mut cache)
.unwrap();
let _call = Scanner::with_pattern_cache(&[r"\)", r#""[^"]*""#], &config, &mut cache)
.unwrap();
assert_eq!(cache.len(), 3); // the string pattern was compiled once§Reuse scanner preparation across starts
The optional precompiled Cargo feature exports compiled scanner programs
for later processes. precompiled-eager also stores prepared scanner-group
instructions, so compatible native and WebAssembly builds can reuse that
Eager preparation. The host can embed the paired archives or deliver them
as application assets. This works through ScannerPatternCache, including
for a scanner with one expression; Regex has no archive constructor.
PrecompiledEagerRecords also accepts individually supplied records from
a versioned source index, so hosts can fetch selectively without assembling
subset archives. Requested records decode and validate on demand. Missing
or incompatible groups use normal Eager preparation; search scratch space
and mutable caches remain local. Keep the original patterns and compatible
compile settings.
See prepared Eager archives
for the producer/consumer workflow and format contract.
§Untrusted input
Some pattern and input pairs take exponential time in a backtracking engine.
(a+)+b against a long run of a is the classic case. When patterns or text
come from users, bound each search with SearchOptions and the *_with
methods. They report a search that hit a limit as an error, where the plain
methods report no match:
use std::time::Duration;
use ferroni::{Regex, RegexError, SearchOptions};
let re = Regex::new(r"(a+)+b").unwrap();
let options = SearchOptions::new().timeout(Duration::from_millis(50));
let hostile = "a".repeat(40);
assert!(matches!(re.find_with(&hostile, options), Err(RegexError::TimeLimitOver)));SearchOptions::retry_limit_in_match counts backtracks instead of time, so
it stops a search at the same point on every machine. For patterns under
review, RegexBuilder::reject_backtracking_risks turns the compile-time
check into an error, and Regex::backtracking_warnings lists its findings:
use ferroni::Regex;
assert!(Regex::builder(r"(a+)+$").reject_backtracking_risks(true).build().is_err());
assert_eq!(Regex::new(r"(a+)+$").unwrap().backtracking_warnings().len(), 1);The untrusted input guide covers the defaults and the other layers of protection.
§Thread safety
Regex is Send + Sync and cheap to Clone. Clones share one compiled
program, so compile once and give each thread a clone. Scanner is Send
and Sync too, but its search takes &mut self, so each thread needs its own.
use std::thread;
use ferroni::Regex;
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<Regex>();
let re = Regex::new(r"\d+").unwrap();
let workers: Vec<_> = (0..4)
.map(|i| {
let re = re.clone();
thread::spawn(move || re.is_match(&format!("job {i}")))
})
.collect();
assert!(workers.into_iter().all(|worker| worker.join().unwrap()));§Low-level C API
The ported C entry points retain their original names. Rust ownership replaces
C allocation and cleanup functions; the
compatibility guide
lists those omissions. onig_new compiles a pattern and onig_search searches
it. Use these when you need something the idiomatic
layer does not expose yet
(guide):
use ferroni::regcomp::onig_new;
use ferroni::regexec::onig_search;
use ferroni::oniguruma::*;
use ferroni::regsyntax::OnigSyntaxOniguruma;
let reg = onig_new(
b"\\d{4}-\\d{2}-\\d{2}",
ONIG_OPTION_NONE,
&ferroni::encodings::utf8::ONIG_ENCODING_UTF8,
&OnigSyntaxOniguruma,
).unwrap();
let input = b"Date: 2026-02-12";
let (result, region) = onig_search(
®, input, input.len(), 0, input.len(),
Some(OnigRegion::new()), ONIG_OPTION_NONE,
);
assert!(result >= 0);
assert_eq!(result, 6); // match starts at byte 6§Modules
The supported surface is the Rust API in the modules below and the C API that
oniguruma.h declares, at the module paths of the C-port table. Everything else
is crate-private: C Oniguruma keeps its own internal headers private too
(ADR-005).
§Start here
| Module | Purpose |
|---|---|
prelude | The types most programs need, also re-exported at the crate root |
api | Regex, RegexBuilder, Match, Captures and the iterators |
replace | The Replacer trait and the iterators behind replace and split |
scanner | Scanner and ScannerPatternCache for multi-pattern tokenization |
error | RegexError, the typed error of every fallible call |
§C port, 1:1 with Oniguruma (ADR-001)
Each C source file maps to one Rust module:
| C File | Rust Module | Purpose |
|---|---|---|
oniguruma.h | oniguruma | Public types, option flags and error codes |
regparse.c | regparse | Pattern parser |
regparse.h | crate-private | Parse tree and parser types |
regcomp.c | regcomp | AST-to-bytecode compiler (onig_new) |
regexec.c | regexec | VM executor (onig_search) |
regexec.c, RegSet section | regset | Multi-regex search (RegSet) |
regint.h | crate-private | Internal types and opcodes; RegexType is re-exported from oniguruma |
regenc.h, regenc.c | regenc | Encoding trait |
utf8.c, ascii.c | encodings | UTF-8 and US-ASCII encodings |
unicode.c | unicode | Character properties and case folding |
regsyntax.c | regsyntax | Syntax definitions |
regerror.c | regerror | Error messages |
regtrav.c | regtrav | Capture tree traversal |
backtrack_lint and backtrack_rewrite are Rust-only
(ADR-008). They define
the types that Regex::backtracking_warnings and
Regex::backtracking_rewrites return.
Re-exports§
pub use prelude::*;
Modules§
- api
- The idiomatic Rust API.
Regexcompiles and searches a pattern,RegexBuildersets its options and syntax, andMatch,CapturesandFindIterreport what a search found. It wraps the C-port functionsonig_newandonig_search. - backtrack_
lint - Rust-only (ADR-008): compile-time warnings for patterns that can backtrack catastrophically.
- backtrack_
rewrite - Opt-in, result-preserving AST rewrites (ADR-008).
- encodings
- The encodings that Ferroni supports: UTF-8 and US-ASCII, ported from
utf8.candascii.c. PassONIG_ENCODING_UTF8orONIG_ENCODING_ASCIItoonig_new. No other encoding is ported (ADR-003). - error
- Idiomatic error types.
RegexErrorgroups the roughly 100 C-stylei32error codes into semantic variants, and keeps each original code for interop. - oniguruma
- Port of
oniguruma.h: the public types, option flags, syntax flags, error codes and structs of the C API.RegexTypeis the compiled pattern, which C callsregex_t. - prelude
- Re-exports of the types most programs need.
use ferroni::prelude::*;brings inRegex,RegexBuilder,Match,Captures, the replacement and split types, and the scanner types. - regcomp
- Port of
regcomp.c: the compiler, which turns the AST (node trees) into bytecode (operation arrays).onig_newis its entry point, andRegexBuilderis the idiomatic way to compile a pattern. - regenc
- Port of
regenc.handregenc.c: theEncodingtrait, which replaces C’sOnigEncodingTypefunction table, and the helpers that the encodings share. - regerror
- Port of
regerror.c: turns an error code into the message that C Oniguruma prints. Themessagefields ofRegexErroruse the same text. - regexec
- Port of
regexec.c: the bytecode interpreter, withonig_matchandonig_search.Regexis the idiomatic search API. - regparse
- Port of
regparse.c: the parser, which turns a pattern (a byte string) into the AST of node trees thatregcompcompiles. It also holds the global warning hooks and the parse limits. - regset
- Port of the
USE_REGSETsection ofregexec.c: multi-regex search over a set of compiled patterns, for syntax highlighters and text editors.Scannerruns on it. - regsyntax
- Port of
regsyntax.c, and of the Oniguruma and Ruby syntaxes fromregparse.c. It holds oneOnigSyntax*static per language and the functions that read and change a syntax. Pass a static toRegexBuilder::syntax, or pass a typedSyntaxtoRegexBuilder::syntax_mode. - regtrav
- Port of
regtrav.c: traversal of the capture tree that capture history records, throughonig_capture_tree_traverse. - replace
- Replacement and splitting for the idiomatic API. The
Replacertrait drivesRegex::replace_alland its relatives, andSplitand its relatives backRegex::split. They follow the shape of theregexcrate and walk the same matches asRegex::find_iterandRegex::captures_iter. - scanner
- High-level multi-pattern scanner for syntax highlighters.
Scannerreports the match that starts earliest among its patterns, with the interface of vscode-oniguruma’sOnigScanner, which Shiki and other vscode-textmate based highlighters use.ScannerPatternCachelets several scanners share their compiled patterns. - unicode
- Port of
unicode.c: Unicode character properties, case folding, and related functions.