Compatibility
A pattern that works in C Oniguruma should work the same way in Ferroni. This page says what that promise covers, where Ferroni differs on purpose, how the promise is checked, and how the port is built to keep it.
Why a line-by-line port
Ferroni began as the regex engine for Ferriki, a
Shiki-compatible syntax highlighter in Rust. TextMate grammars are written for
Oniguruma: they rely on look-behind, \G anchors, backreferences and
quantifier spellings that most other engines reject or read differently.
Bindings to the C library would have kept the C code, the C toolchain and its memory-safety record. A new engine would have broken grammars in subtle ways. So Ferroni ports Oniguruma itself, file by file and function by function, and verifies the result against Oniguruma's own test suite (ADR-001).
What Ferroni covers
Ferroni ports ASCII and UTF-8, two of Oniguruma's 29 encodings (ADR-003). All twelve syntax modes are included, from Oniguruma and Ruby to Perl, Python, Java and POSIX. For the projects built on Oniguruma's syntax, that means:
| Project | Status | What it means |
|---|---|---|
| TextMate grammars, VS Code, Shiki | Covered | vscode-oniguruma compiles every pattern as UTF-8. Ferroni's scanner keeps its API and UTF-16 offsets. |
| jq | Covered | jq matches on UTF-8 text, which Ferroni handles in full. |
| PHP mbregex | UTF-8 only | mb_ereg in Shift_JIS, EUC-JP or another non-UTF-8 encoding is not covered. |
| Ruby | Not a target | Ruby runs Onigmo, a fork of Oniguruma with its own history and encodings. |
These parts of the C library are not ported:
- 27 of 29 encodings: UTF-16 and UTF-32, Shift_JIS, EUC-JP, EUC-KR, EUC-TW, EUC-CN, Big5, GB18030, KOI8-R, CP1251, and ISO-8859-1 to -16.
- The POSIX and GNU APIs:
regcomp,regexecandregfree(ADR-012). - 20 of the 152 functions that
oniguruma.hdeclares, all of them C memory, string and setup plumbing that Rust does not need. The other 132 are public under their C names.onig_new_deluxe,onig_new_without_allocandonig_reg_initallocate and initialize aregex_tin C; useonig_new.onig_free,onig_free_body,onig_region_free,onig_regset_free,onig_free_match_paramandonig_free_match_param_contentfree what Rust'sDropfrees.onig_initialize_encoding,onig_copy_encoding,onigenc_init,onigenc_get_default_encoding,onigenc_set_default_encodingandonigenc_set_default_caseconv_tableset up encoding structs and C's process-wide encoding defaults; Ferroni's two encodings are the staticsONIG_ENCODING_UTF8andONIG_ENCODING_ASCII, passed toonig_new.onigenc_strdup,onigenc_str_bytelen_nullandonigenc_get_right_adjust_char_head_with_prevare helpers over C's NUL-terminated buffers; Rust slices carry their length.onig_null_warnis C's no-op warning callback;onig_set_warn_functakes anyfn(&str).onig_setup_builtin_monitors_by_ascii_encoded_nameregisters monitor callouts that print to a CFILE*.
Where Ferroni differs on purpose
- Unicode 18.0. Ferroni generates its tables from the Unicode Character
Database 18.0; C Oniguruma's last release ships 16.0. Some characters change
properties as a result: 672 leave
Extended_Pictographic, U+0295 moves fromLltoLo, and some\Xboundaries move. U+1DF95 folds to "ss", so(?i)ssmatches it (ADR-015). - A capped backtracking stack. When no stack limit is set, C lets the backtracking stack grow until memory runs out. Ferroni stops at 10 million entries and reports the limit (untrusted input).
- Additions C never had. The Scanner, per-search limits, the compile-time
backtracking check and the idiomatic
RegexAPI sit beside the port and do not change how a pattern matches.
How compatibility is checked
Test parity
Every upstream test that targets UTF-8 is ported case by case. The EUC-JP tests are out of scope with that encoding.
| Upstream file | Encoding | Upstream | Ferroni | Status |
|---|---|---|---|---|
test_utf8.c | UTF-8 | 1,554 | 1,561 | 100% |
test_back.c | UTF-8 | 1,225 | 1,225 | 100% |
test_syntax.c | UTF-8 | 144 | 144 | 100% |
test_options.c | UTF-8 | 47 | 48 | 100% |
test_regset.c | UTF-8 | 4 | 15 | 100% |
testc.c | EUC-JP | 785 | — | out of scope |
| C total (UTF-8) | 2,974 | 2,993 | 100% | |
| vscode-oniguruma tests | UTF-16 | 15 | 25 | 100% |
The Ferroni column can be higher than the upstream one: some upstream cases
are split into several Rust tests. On top of the ported cases, Ferroni adds
Rust-native tests for the idiomatic API, edge cases, error paths and coverage
gaps. The tree holds at least 2,500 #[test] functions, which
scripts/count-tests.sh
derives from the tree. The test strategy is recorded in
ADR-011.
Coverage, fuzzing and benchmark checks
- Coverage gate. C Oniguruma has no coverage reporting. Ferroni measures
line coverage with cargo-llvm-cov
on every pull request, and
scripts/coverage.shfails the build below its threshold. Generated Unicode tables, the FFI bindings and the test files are excluded from the measurement. - Fuzzing. Three cargo-fuzz targets cover pattern compilation, matching and the Scanner API. Each runs briefly on every pull request and for 15 minutes every week.
- Results before timings. Every production regex case must reproduce its source-runtime oracle before timing. C Oniguruma also runs the admitted tasks; all engines share explicit operation contracts (regex workloads).
The bugs found while porting, and what they teach about C-to-Rust ports, are collected in ADR-014.
How the port maps to C
Each C source file maps to one Rust module, with the same function names and control flow (ADR-001, ADR-004). A fix in upstream C can be found and compared in the corresponding Rust function.
| C file | Rust module | Purpose |
|---|---|---|
regparse.c | regparse.rs | Pattern parser |
regcomp.c | regcomp.rs | AST-to-bytecode compiler |
regexec.c | regexec.rs | VM executor |
regint.h | regint.rs | Internal types and opcodes (crate-private) |
oniguruma.h | oniguruma.rs | Public types and constants |
regenc.c | regenc.rs | Encoding trait |
regsyntax.c | regsyntax.rs | The 12 syntax definitions |
regset.c | regset.rs | Multi-regex search (RegSet) |
regerror.c | regerror.rs | Error messages |
regtrav.c | regtrav.rs | Capture tree traversal |
unicode.c | unicode/mod.rs | Unicode tables and segmentation |
| — | scanner.rs | Multi-pattern scanner for syntax highlighting |
| — | api.rs | Idiomatic Rust API |
The compilation pipeline is the same as in C:
onig_new() -> onig_compile()
-> onig_parse_tree() (pattern -> AST)
-> reduce_string_list() (merge adjacent strings)
-> tune_tree() (6 optimization sub-passes)
-> compile_tree() (AST -> VM bytecode)
-> set_optimize_info() (extract search strategy)
Optimizations that C does not have are recorded separately, so the parity of the ported code stays reviewable (ADR-008).