Skip to main content

Crate ferroni

Crate ferroni 

Source
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(
    &reg, 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

ModulePurpose
preludeThe types most programs need, also re-exported at the crate root
apiRegex, RegexBuilder, Match, Captures and the iterators
replaceThe Replacer trait and the iterators behind replace and split
scannerScanner and ScannerPatternCache for multi-pattern tokenization
errorRegexError, 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 FileRust ModulePurpose
oniguruma.honigurumaPublic types, option flags and error codes
regparse.cregparsePattern parser
regparse.hcrate-privateParse tree and parser types
regcomp.cregcompAST-to-bytecode compiler (onig_new)
regexec.cregexecVM executor (onig_search)
regexec.c, RegSet sectionregsetMulti-regex search (RegSet)
regint.hcrate-privateInternal types and opcodes; RegexType is re-exported from oniguruma
regenc.h, regenc.cregencEncoding trait
utf8.c, ascii.cencodingsUTF-8 and US-ASCII encodings
unicode.cunicodeCharacter properties and case folding
regsyntax.cregsyntaxSyntax definitions
regerror.cregerrorError messages
regtrav.cregtravCapture 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. Regex compiles and searches a pattern, RegexBuilder sets its options and syntax, and Match, Captures and FindIter report what a search found. It wraps the C-port functions onig_new and onig_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.c and ascii.c. Pass ONIG_ENCODING_UTF8 or ONIG_ENCODING_ASCII to onig_new. No other encoding is ported (ADR-003).
error
Idiomatic error types. RegexError groups the roughly 100 C-style i32 error 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. RegexType is the compiled pattern, which C calls regex_t.
prelude
Re-exports of the types most programs need. use ferroni::prelude::*; brings in Regex, 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_new is its entry point, and RegexBuilder is the idiomatic way to compile a pattern.
regenc
Port of regenc.h and regenc.c: the Encoding trait, which replaces C’s OnigEncodingType function table, and the helpers that the encodings share.
regerror
Port of regerror.c: turns an error code into the message that C Oniguruma prints. The message fields of RegexError use the same text.
regexec
Port of regexec.c: the bytecode interpreter, with onig_match and onig_search. Regex is the idiomatic search API.
regparse
Port of regparse.c: the parser, which turns a pattern (a byte string) into the AST of node trees that regcomp compiles. It also holds the global warning hooks and the parse limits.
regset
Port of the USE_REGSET section of regexec.c: multi-regex search over a set of compiled patterns, for syntax highlighters and text editors. Scanner runs on it.
regsyntax
Port of regsyntax.c, and of the Oniguruma and Ruby syntaxes from regparse.c. It holds one OnigSyntax* static per language and the functions that read and change a syntax. Pass a static to RegexBuilder::syntax, or pass a typed Syntax to RegexBuilder::syntax_mode.
regtrav
Port of regtrav.c: traversal of the capture tree that capture history records, through onig_capture_tree_traverse.
replace
Replacement and splitting for the idiomatic API. The Replacer trait drives Regex::replace_all and its relatives, and Split and its relatives back Regex::split. They follow the shape of the regex crate and walk the same matches as Regex::find_iter and Regex::captures_iter.
scanner
High-level multi-pattern scanner for syntax highlighters. Scanner reports the match that starts earliest among its patterns, with the interface of vscode-oniguruma’s OnigScanner, which Shiki and other vscode-textmate based highlighters use. ScannerPatternCache lets several scanners share their compiled patterns.
unicode
Port of unicode.c: Unicode character properties, case folding, and related functions.