Skip to main content

tiny_regex/
raw.rs

1//! Low-level compiled-regex type and FFI bindings.
2//!
3//! Most users should use [`Regex`][crate::Regex] or
4//! [`RegexBuf`][crate::RegexBuf] from the crate root rather than importing
5//! from this module directly. The module is public so that `RegexBuf<N, CCL,
6//! MEMO>` with non-default parameters can be constructed without a glob import.
7#![allow(non_camel_case_types, non_snake_case, non_upper_case_globals)]
8
9use core::ffi::CStr;
10
11include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
12
13/// Default node capacity: max pattern symbols in a compiled regex.
14pub const DEFAULT_NODES: usize = 32;
15/// Default character-class buffer size in bytes.
16pub const DEFAULT_CCL: usize = 64;
17/// Text-offset dimension used to compute [`DEFAULT_MEMO`].
18///
19/// Useful when sizing a custom `MEMO` for non-default `N`:
20/// `MEMO = (N * DEFAULT_MATCH_TEXT_LEN + 7) / 8`.
21pub const DEFAULT_MATCH_TEXT_LEN: usize = 64;
22/// Memo table size (bytes) for the default node and text-length capacities.
23///
24/// Computed as `(DEFAULT_NODES × DEFAULT_MATCH_TEXT_LEN + 7) / 8`.
25pub const DEFAULT_MEMO: usize =
26    (DEFAULT_NODES * DEFAULT_MATCH_TEXT_LEN + 7) / 8;
27
28
29/// An owned, compiled regex pattern with compile-time-fixed storage.
30///
31/// - `N` — node capacity: max compiled pattern symbols.
32/// - `CCL` — character-class buffer size in bytes.
33/// - `MEMO` — memo table size in bytes; use `(N * DEFAULT_MATCH_TEXT_LEN + 7) / 8`.
34///
35/// For the common case use [`Regex`][crate::Regex] (a type alias for
36/// `RegexBuf<32, 64, 256>`) so you never spell out the parameters.
37/// Use `RegexBuf` directly only when you need non-default sizes.
38///
39/// Created via [`RegexBuf::new`]; the pattern can be updated in-place via
40/// [`RegexBuf::recompile`]. Use [`find_at`][RegexBuf::find_at] and
41/// [`find_iter`][RegexBuf::find_iter] to search.
42///
43/// A `RegexBuf` is always valid — construction fails explicitly via `None`.
44/// Each `find_at` call stack-allocates and zeros a `[u8; MEMO]` memo table
45/// independently, so `RegexBuf` is `Send + Sync` without unsafe.
46pub struct RegexBuf<
47    const N: usize = DEFAULT_NODES,
48    const CCL: usize = DEFAULT_CCL,
49    const MEMO: usize = DEFAULT_MEMO,
50> {
51    /// Compiled pattern nodes — char-class entries carry a byte *offset*
52    /// into `ccl` (not a raw pointer), so the struct is freely movable.
53    re_nodes: [regex_t; N],
54    /// Character-class byte buffer referenced by CHAR_CLASS/INV_CHAR_CLASS
55    /// nodes via their `u.ccl` byte offset.
56    ccl: [u8; CCL],
57}
58
59// regex_t.u.ccl is a size_t offset into ccl_buf, not a pointer.
60// Both fields contain no raw pointers, so Send + Sync hold automatically.
61
62impl<const N: usize, const CCL: usize, const MEMO: usize> RegexBuf<N, CCL, MEMO> {
63    fn compile_into(re_nodes: &mut [regex_t; N], ccl: &mut [u8; CCL], pattern: &CStr) -> bool {
64        // Zero-fill ccl so ccl_buf[0]=='\0', satisfying the sentinel required by
65        // matchcharclass (reads str[-1] to detect a literal '-' at the start of
66        // a character class; str is always >=1 into ccl_buf, so [-1] is ccl_buf[0]).
67        ccl.fill(0);
68        // SAFETY: re_nodes is our exclusively owned array; re_compile writes into
69        // it up to N nodes.  ccl and pattern are valid for their lifetimes.
70        let bytes_written = unsafe {
71            re_compile(
72                re_nodes.as_mut_ptr() as *mut regex_t,
73                N,
74                ccl.as_mut_ptr(),
75                CCL,
76                pattern.as_ptr(),
77            )
78        };
79        bytes_written > 0
80    }
81
82    /// Compile `pattern` into a new `RegexBuf`.
83    ///
84    /// Returns `None` if `pattern` is syntactically invalid or exceeds the
85    /// node capacity `N` or character-class buffer `CCL`.
86    pub fn new(pattern: &CStr) -> Option<RegexBuf<N, CCL, MEMO>> {
87        // SAFETY: regex_t is a C struct with unsigned char type + union
88        // {u8, size_t}; all-zero bytes are a valid representation (type=0 is
89        // UNUSED; union ccl offset 0 refers to the sentinel position in
90        // ccl_buf and is safe to read as an empty class).
91        let mut re_nodes: [regex_t; N] = unsafe { core::mem::zeroed() };
92        let mut ccl = [0u8; CCL];
93        if Self::compile_into(&mut re_nodes, &mut ccl, pattern) {
94            Some(RegexBuf { re_nodes, ccl })
95        } else {
96            None
97        }
98    }
99
100    /// Recompile this `RegexBuf` with a new `pattern`, reusing existing storage.
101    ///
102    /// Returns `Some` on success. On failure returns `None` and `self` is
103    /// dropped — the previous compiled pattern is lost.
104    pub fn recompile(mut self, pattern: &CStr) -> Option<RegexBuf<N, CCL, MEMO>> {
105        if Self::compile_into(&mut self.re_nodes, &mut self.ccl, pattern) {
106            Some(self)
107        } else {
108            None
109        }
110    }
111
112    /// Match starting at byte offset `start` inside `haystack`.
113    ///
114    /// Stack-allocates `[u8; MEMO]` per call for an independent, zero-initialised
115    /// memo table.
116    ///
117    /// Returns the absolute match start and writes match length into
118    /// `match_length`, or -1 on no match.
119    pub(crate) fn matchp_at(
120        &self,
121        haystack: &CStr,
122        start: usize,
123        match_length: &mut i32,
124    ) -> i32 {
125        if start > haystack.to_bytes().len() || start > i32::MAX as usize {
126            *match_length = 0;
127            return -1;
128        }
129        // SAFETY: start <= haystack.to_bytes().len() — within allocation;
130        // null terminator still terminates the sub-CStr.
131        let sub = unsafe { CStr::from_ptr(haystack.as_ptr().add(start)) };
132        let mut len: i32 = 0;
133        // MEMO is a const generic stack array; MEMO==0 gives a zero-length array.
134        // C normalises memo_bytes==0 to NULL before any dereference, so passing
135        // as_mut_ptr() unconditionally is safe even for the zero-length case.
136        let mut memo = [0u8; MEMO];
137        // memo_stride is the text-offset dimension of the memo bit array:
138        // MEMO bytes hold N*memo_stride bits, so stride = MEMO*8/N.
139        // When MEMO==0 memoisation is disabled; stride is unused by C.
140        let memo_stride = if MEMO == 0 { 0 } else { MEMO * 8 / N };
141        // SAFETY: re_nodes holds a valid compiled pattern.  ccl is the
142        // char-class buffer; entries reference it by byte offset, so moving
143        // RegexBuf never dangles anything.  memo is a zeroed stack array;
144        // C treats memo_bytes==0 as disabled without dereferencing the pointer.
145        let ret = unsafe {
146            re_matchp(
147                self.re_nodes.as_ptr() as re_t,
148                self.ccl.as_ptr(),
149                sub.as_ptr(),
150                &mut len,
151                memo.as_mut_ptr(),
152                MEMO,
153                memo_stride,
154            )
155        };
156        *match_length = len;
157        if ret < 0 { return -1; }
158        ret + start as i32
159    }
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165
166    // Finding 3: matchcharclass reads str[-1] to detect a literal '-' at the start
167    // of a character class.  That byte is ccl_buf[0], which must be '\0'.
168    // The invariant is upheld because compile_into() zero-fills ccl before every
169    // compilation, but nothing in the C code documents or asserts it.
170    //
171    // This test proves the dependency: after RegexBuf::new("[-abc]"), ccl[0] is 0
172    // and '-' correctly matches as a literal.  Corrupting ccl[0] to any non-zero
173    // byte breaks the match — demonstrating that the sentinel must be '\0'.
174    #[test]
175    fn matchcharclass_correctness_depends_on_ccl_sentinel_byte() {
176        use core::ffi::CStr;
177        // Use the default-sized alias so the test is readable.
178        type R = RegexBuf;
179        let mut re = R::new(c"[-abc]").unwrap();
180
181        let mut ml = 0i32;
182        assert!(re.matchp_at(CStr::from_bytes_with_nul(b"-\0").unwrap(), 0, &mut ml) >= 0,
183            "dash should match as a literal when ccl[0] == 0");
184
185        re.ccl[0] = b'x';
186        let mut ml2 = 0i32;
187        assert!(re.matchp_at(CStr::from_bytes_with_nul(b"-\0").unwrap(), 0, &mut ml2) < 0,
188            "dash fails to match after sentinel byte is corrupted — proves str[-1] dependency");
189    }
190}