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!("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}