rusty_expressions 0.2.1

Oniguruma remade in pure Rust: named groups, look-around, backreferences, subexp calls, absent expressions, callouts, per-regex encodings (UTF-8/16/32, Shift_JIS, Big5, EUC-*, GB18030, ISO-8859-*) and Perl/Python/Java/POSIX/GNU/Emacs/grep syntax dialects. Match-equivalent to Oniguruma 6.9.10 and ~3x faster than libonig. no_std + alloc with default-features = false, runs on wasm32, no C toolchain.
Documentation
//! Optional Oniguruma-shaped C ABI (`onig_new` / `regex_t`).
//!
//! Pure Rust `extern "C"` wrappers. No libonig, no C toolchain, no `onig-sys`.
//! Encoding and syntax are integer ids, not C pointers.

// A C ABI cannot be expressed in safe Rust: every entry point takes raw
// pointers supplied by a C caller. The `unsafe` here is confined to this
// module -- the engine itself is `unsafe_code = "deny"` -- and each function
// documents the invariants the caller must uphold.
#![allow(unsafe_code)]

extern crate alloc;

use alloc::boxed::Box;
use alloc::vec::Vec;
use core::ffi::{c_int, c_uint};
use core::ptr;
use core::slice;

use super::{Encoding, MatchParam, Options, Regex, Region, Syntax};

pub const ONIG_NORMAL: c_int = 0;
pub const ONIG_MISMATCH: c_int = -1;
pub const ONIGERR_INVALID_ARGUMENT: c_int = -6;
pub const ONIGERR_MEMORY: c_int = -5;
pub const ONIGERR_COMPILE: c_int = -100;

pub const ONIG_ENCODING_ASCII: c_uint = 1;
pub const ONIG_ENCODING_UTF8: c_uint = 2;
pub const ONIG_ENCODING_UTF16_BE: c_uint = 3;
pub const ONIG_ENCODING_UTF16_LE: c_uint = 4;
pub const ONIG_ENCODING_UTF32_BE: c_uint = 5;
pub const ONIG_ENCODING_UTF32_LE: c_uint = 6;
pub const ONIG_ENCODING_ISO_8859_1: c_uint = 7;
pub const ONIG_ENCODING_EUC_JP: c_uint = 8;
pub const ONIG_ENCODING_SJIS: c_uint = 9;
pub const ONIG_ENCODING_BIG5: c_uint = 10;
pub const ONIG_ENCODING_GB18030: c_uint = 11;
pub const ONIG_ENCODING_KOI8_R: c_uint = 12;
pub const ONIG_ENCODING_CP1251: c_uint = 13;

pub const ONIG_SYNTAX_ONIGURUMA: c_uint = 0;
pub const ONIG_SYNTAX_POSIX_BASIC: c_uint = 1;
pub const ONIG_SYNTAX_POSIX_EXTENDED: c_uint = 2;
pub const ONIG_SYNTAX_PERL: c_uint = 3;
pub const ONIG_SYNTAX_PERL_NG: c_uint = 4;
pub const ONIG_SYNTAX_JAVA: c_uint = 5;
pub const ONIG_SYNTAX_PYTHON: c_uint = 6;
pub const ONIG_SYNTAX_GNU_REGEX: c_uint = 7;
pub const ONIG_SYNTAX_EMACS: c_uint = 8;
pub const ONIG_SYNTAX_GREP: c_uint = 9;
pub const ONIG_SYNTAX_ASIS: c_uint = 10;

/// Oniguruma `OnigErrorInfo` (pattern snapshot on compile failure).
#[repr(C)]
pub struct OnigErrorInfo {
    pub enc: c_uint,
    pub par: *const u8,
    pub par_end: *const u8,
}

/// Oniguruma `OnigRegion` (byte offsets). `history` is Rust-side only.
#[repr(C)]
pub struct OnigRegion {
    pub allocated: c_int,
    pub num_regs: c_int,
    pub beg: *mut c_int,
    pub end: *mut c_int,
}

fn enc_from_id(id: c_uint) -> Encoding {
    match id {
        ONIG_ENCODING_ASCII => Encoding::ASCII,
        ONIG_ENCODING_UTF16_BE => Encoding::UTF16_BE,
        ONIG_ENCODING_UTF16_LE => Encoding::UTF16_LE,
        ONIG_ENCODING_UTF32_BE => Encoding::UTF32_BE,
        ONIG_ENCODING_UTF32_LE => Encoding::UTF32_LE,
        ONIG_ENCODING_ISO_8859_1 => Encoding::ISO_8859_1,
        ONIG_ENCODING_EUC_JP => Encoding::EUC_JP,
        ONIG_ENCODING_SJIS => Encoding::SJIS,
        ONIG_ENCODING_BIG5 => Encoding::BIG5,
        ONIG_ENCODING_GB18030 => Encoding::GB18030,
        ONIG_ENCODING_KOI8_R => Encoding::KOI8_R,
        ONIG_ENCODING_CP1251 => Encoding::CP1251,
        _ => Encoding::UTF8,
    }
}

fn syntax_from_id(id: c_uint) -> Syntax {
    match id {
        ONIG_SYNTAX_POSIX_BASIC => Syntax::posix_basic(),
        ONIG_SYNTAX_POSIX_EXTENDED => Syntax::posix_extended(),
        ONIG_SYNTAX_PERL => Syntax::perl(),
        ONIG_SYNTAX_PERL_NG => Syntax::perl_ng(),
        ONIG_SYNTAX_JAVA => Syntax::java(),
        ONIG_SYNTAX_PYTHON => Syntax::python(),
        ONIG_SYNTAX_GNU_REGEX => Syntax::gnu_regex(),
        ONIG_SYNTAX_EMACS => Syntax::emacs(),
        ONIG_SYNTAX_GREP => Syntax::grep(),
        ONIG_SYNTAX_ASIS => Syntax::ASIS,
        _ => Syntax::ONIGURUMA,
    }
}

unsafe fn ptr_span<'a>(a: *const u8, b: *const u8) -> Option<&'a [u8]> {
    if a.is_null() || b.is_null() {
        return None;
    }
    let n = b.offset_from(a);
    if n < 0 {
        return None;
    }
    Some(slice::from_raw_parts(a, n as usize))
}

unsafe fn fill_region(dst: *mut OnigRegion, m: &Region) {
    if dst.is_null() {
        return;
    }
    onig_region_clear(dst);
    let n = m.captures.len();
    let mut beg = Vec::with_capacity(n);
    let mut end = Vec::with_capacity(n);
    for cap in &m.captures {
        match cap {
            Some(r) => {
                beg.push(r.start as c_int);
                end.push(r.end as c_int);
            }
            None => {
                beg.push(-1);
                end.push(-1);
            }
        }
    }
    let beg = beg.into_boxed_slice();
    let end = end.into_boxed_slice();
    let r = &mut *dst;
    r.num_regs = n as c_int;
    r.allocated = n as c_int;
    r.beg = Box::into_raw(beg) as *mut c_int;
    r.end = Box::into_raw(end) as *mut c_int;
}

/// No-op (`onig_initialize`). Encodings are values, not process-global tables.
///
/// # Safety
///
/// Both arguments are ignored, so any values are accepted. Kept so C callers
/// written against Oniguruma link and run unchanged.
#[no_mangle]
pub unsafe extern "C" fn onig_initialize(_encodings: *mut c_uint, _n: c_int) -> c_int {
    ONIG_NORMAL
}

/// No-op (`onig_end`).
///
/// # Safety
///
/// Takes no arguments and touches no state; always safe to call, in any order,
/// any number of times.
#[no_mangle]
pub unsafe extern "C" fn onig_end() -> c_int {
    ONIG_NORMAL
}

/// Compile a pattern into a heap `regex_t` (`Box<Regex>`).
///
/// # Safety
///
/// `reg` must be a valid, writable `*mut *mut Regex`; a null one is rejected
/// rather than written through. `pattern` and `pattern_end` must bound the
/// same allocation with `pattern <= pattern_end` -- a reversed or foreign pair
/// is rejected, not read. `einfo` may be null; when it is not, it must be
/// writable for one `OnigErrorInfo`.
///
/// On success `*reg` owns a regex that must be released with [`onig_free`]
/// exactly once.
#[no_mangle]
pub unsafe extern "C" fn onig_new(
    reg: *mut *mut Regex,
    pattern: *const u8,
    pattern_end: *const u8,
    option: c_uint,
    enc: c_uint,
    syntax: c_uint,
    einfo: *mut OnigErrorInfo,
) -> c_int {
    if reg.is_null() {
        return ONIGERR_INVALID_ARGUMENT;
    }
    let Some(pat) = ptr_span(pattern, pattern_end) else {
        return ONIGERR_INVALID_ARGUMENT;
    };
    match Regex::new(pat, Options(option), enc_from_id(enc), syntax_from_id(syntax)) {
        Ok(r) => {
            *reg = Box::into_raw(Box::new(r));
            ONIG_NORMAL
        }
        Err(_) => {
            if !einfo.is_null() {
                (*einfo).enc = enc;
                (*einfo).par = pattern;
                (*einfo).par_end = pattern_end;
            }
            ONIGERR_COMPILE
        }
    }
}

/// Free a regex from `onig_new`.
///
/// # Safety
///
/// `reg` must be null, or a pointer returned by [`onig_new`] that has not yet
/// been freed. Freeing null is a no-op, as in Oniguruma. Freeing the same
/// pointer twice is undefined behaviour.
#[no_mangle]
pub unsafe extern "C" fn onig_free(reg: *mut Regex) {
    if !reg.is_null() {
        drop(Box::from_raw(reg));
    }
}

/// Search (`onig_search`). Returns match start offset, `ONIG_MISMATCH`, or an error.
///
/// # Safety
///
/// `reg` must be null or a live regex from [`onig_new`]; null is rejected.
/// `str` and `end` must bound one allocation. `start` and `range` must be null
/// or point into that same allocation -- offsets outside the haystack are
/// rejected with `ONIGERR_INVALID_ARGUMENT`, but the pointers themselves must
/// still be derived from it. `region` may be null; when it is not, it must be
/// a live `OnigRegion` from [`onig_region_new`], whose previous contents this
/// call frees before writing.
#[no_mangle]
pub unsafe extern "C" fn onig_search(
    reg: *const Regex,
    str: *const u8,
    end: *const u8,
    start: *const u8,
    range: *const u8,
    region: *mut OnigRegion,
    _option: c_uint,
) -> c_int {
    if reg.is_null() {
        return ONIGERR_INVALID_ARGUMENT;
    }
    let Some(hay) = ptr_span(str, end) else {
        return ONIGERR_INVALID_ARGUMENT;
    };
    let st = if start.is_null() {
        0
    } else {
        start.offset_from(str)
    };
    let rg = if range.is_null() {
        hay.len() as isize
    } else {
        range.offset_from(str)
    };
    if st < 0 || rg < 0 {
        return ONIGERR_INVALID_ARGUMENT;
    }
    match (*reg).search_range_param(hay, st as usize, rg as usize, &MatchParam::default()) {
        Ok(Some(m)) => {
            fill_region(region, &m);
            m.range().start as c_int
        }
        Ok(None) => ONIG_MISMATCH,
        Err(_) => ONIGERR_MEMORY,
    }
}

/// Match only at `at` (`onig_match`).
///
/// # Safety
///
/// As [`onig_search`], with `at` in place of `start`/`range`. An `at` past the
/// end of the haystack is rejected rather than answered: it used to report an
/// empty match at that offset, which a caller would then have sliced with.
#[no_mangle]
pub unsafe extern "C" fn onig_match(
    reg: *const Regex,
    str: *const u8,
    end: *const u8,
    at: *const u8,
    region: *mut OnigRegion,
    _option: c_uint,
) -> c_int {
    if reg.is_null() {
        return ONIGERR_INVALID_ARGUMENT;
    }
    let Some(hay) = ptr_span(str, end) else {
        return ONIGERR_INVALID_ARGUMENT;
    };
    let pos = if at.is_null() {
        0
    } else {
        at.offset_from(str)
    };
    if pos < 0 {
        return ONIGERR_INVALID_ARGUMENT;
    }
    match (*reg).find_at(hay, pos as usize) {
        Ok(Some(m)) => {
            fill_region(region, &m);
            (m.range().end - m.range().start) as c_int
        }
        Ok(None) => ONIG_MISMATCH,
        Err(_) => ONIGERR_MEMORY,
    }
}

/// Allocate an empty region (`onig_region_new`).
///
/// # Safety
///
/// Takes no arguments and is always safe to call. The result must be released
/// with [`onig_region_free`] exactly once.
#[no_mangle]
pub unsafe extern "C" fn onig_region_new() -> *mut OnigRegion {
    Box::into_raw(Box::new(OnigRegion {
        allocated: 0,
        num_regs: 0,
        beg: ptr::null_mut(),
        end: ptr::null_mut(),
    }))
}

/// Release a region's capture arrays, and optionally the region (`onig_region_free`).
///
/// # Safety
///
/// `region` must be null or a live region from [`onig_region_new`]. With
/// `free_self` non-zero the region itself is freed and must not be used again.
/// The capture arrays must be ones this library allocated: a region whose
/// `beg`/`end` were set by the caller must not be passed here.
#[no_mangle]
pub unsafe extern "C" fn onig_region_free(region: *mut OnigRegion, free_self: c_int) {
    if region.is_null() {
        return;
    }
    onig_region_clear(region);
    if free_self != 0 {
        drop(Box::from_raw(region));
    }
}

/// Drop a region's captures, leaving it reusable (`onig_region_clear`).
///
/// # Safety
///
/// `region` must be null or a live region from [`onig_region_new`], with
/// capture arrays this library allocated. Clearing an already-cleared region
/// is a no-op, so repeated calls are safe.
#[no_mangle]
pub unsafe extern "C" fn onig_region_clear(region: *mut OnigRegion) {
    if region.is_null() {
        return;
    }
    let r = &mut *region;
    if !r.beg.is_null() && r.allocated > 0 {
        let n = r.allocated as usize;
        drop(Box::from_raw(ptr::slice_from_raw_parts_mut(r.beg, n)));
        drop(Box::from_raw(ptr::slice_from_raw_parts_mut(r.end, n)));
    }
    r.beg = ptr::null_mut();
    r.end = ptr::null_mut();
    r.allocated = 0;
    r.num_regs = 0;
}