use std::{
collections::HashMap,
panic::{RefUnwindSafe, UnwindSafe},
sync::Arc,
};
use pcre2_sys::{
PCRE2_CASELESS, PCRE2_DOTALL, PCRE2_EXTENDED, PCRE2_MATCH_INVALID_UTF,
PCRE2_MULTILINE, PCRE2_NEWLINE_ANYCRLF, PCRE2_UCP, PCRE2_UNSET, PCRE2_UTF,
};
use crate::{
error::Error,
ffi::{Code, CompileContext, MatchConfig, MatchData},
pool::{Pool, PoolGuard},
};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Match<'s> {
subject: &'s [u8],
start: usize,
end: usize,
}
impl<'s> Match<'s> {
#[inline]
pub fn start(&self) -> usize {
self.start
}
#[inline]
pub fn end(&self) -> usize {
self.end
}
#[inline]
pub fn as_bytes(&self) -> &'s [u8] {
&self.subject[self.start..self.end]
}
fn new(subject: &'s [u8], start: usize, end: usize) -> Match<'s> {
Match { subject, start, end }
}
#[cfg(test)]
fn as_pair(&self) -> (usize, usize) {
(self.start, self.end)
}
}
#[derive(Clone, Debug)]
struct Config {
caseless: bool,
dotall: bool,
extended: bool,
multi_line: bool,
crlf: bool,
ucp: bool,
utf: bool,
jit: JITChoice,
match_config: MatchConfig,
}
#[derive(Clone, Debug)]
enum JITChoice {
Never,
Always,
Attempt,
}
impl Default for Config {
fn default() -> Config {
Config {
caseless: false,
dotall: false,
extended: false,
multi_line: false,
crlf: false,
ucp: false,
utf: false,
jit: JITChoice::Never,
match_config: MatchConfig::default(),
}
}
}
#[derive(Clone, Debug)]
pub struct RegexBuilder {
config: Config,
}
impl RegexBuilder {
pub fn new() -> RegexBuilder {
RegexBuilder { config: Config::default() }
}
pub fn build(&self, pattern: &str) -> Result<Regex, Error> {
let mut options = 0;
if self.config.caseless {
options |= PCRE2_CASELESS;
}
if self.config.dotall {
options |= PCRE2_DOTALL;
}
if self.config.extended {
options |= PCRE2_EXTENDED;
}
if self.config.multi_line {
options |= PCRE2_MULTILINE;
}
if self.config.ucp {
options |= PCRE2_UCP;
options |= PCRE2_UTF;
options |= PCRE2_MATCH_INVALID_UTF;
}
if self.config.utf {
options |= PCRE2_UTF;
}
let mut ctx = CompileContext::new();
if self.config.crlf {
ctx.set_newline(PCRE2_NEWLINE_ANYCRLF)
.expect("PCRE2_NEWLINE_ANYCRLF is a legal value");
}
let mut code = Code::new(pattern, options, ctx)?;
match self.config.jit {
JITChoice::Never => {} JITChoice::Always => {
code.jit_compile()?;
}
JITChoice::Attempt => {
if let Err(err) = code.jit_compile() {
log::debug!("JIT compilation failed: {}", err);
}
}
}
let capture_names = code.capture_names()?;
let mut idx = HashMap::new();
for (i, group) in capture_names.iter().enumerate() {
if let Some(ref name) = *group {
idx.insert(name.to_string(), i);
}
}
let code = Arc::new(code);
let match_data = {
let config = self.config.match_config.clone();
let code = Arc::clone(&code);
let create: MatchDataPoolFn =
Box::new(move || MatchData::new(config.clone(), &code));
Pool::new(create)
};
Ok(Regex {
config: Arc::new(self.config.clone()),
pattern: pattern.to_string(),
code,
capture_names: Arc::new(capture_names),
capture_names_idx: Arc::new(idx),
match_data,
})
}
pub fn caseless(&mut self, yes: bool) -> &mut RegexBuilder {
self.config.caseless = yes;
self
}
pub fn dotall(&mut self, yes: bool) -> &mut RegexBuilder {
self.config.dotall = yes;
self
}
pub fn extended(&mut self, yes: bool) -> &mut RegexBuilder {
self.config.extended = yes;
self
}
pub fn multi_line(&mut self, yes: bool) -> &mut RegexBuilder {
self.config.multi_line = yes;
self
}
pub fn crlf(&mut self, yes: bool) -> &mut RegexBuilder {
self.config.crlf = yes;
self
}
pub fn ucp(&mut self, yes: bool) -> &mut RegexBuilder {
self.config.ucp = yes;
self
}
pub fn utf(&mut self, yes: bool) -> &mut RegexBuilder {
self.config.utf = yes;
self
}
#[deprecated(
since = "0.2.4",
note = "now a no-op due to new PCRE2 features"
)]
pub fn disable_utf_check(&mut self) -> &mut RegexBuilder {
self
}
pub fn jit(&mut self, yes: bool) -> &mut RegexBuilder {
if yes {
self.config.jit = JITChoice::Always;
} else {
self.config.jit = JITChoice::Never;
}
self
}
pub fn jit_if_available(&mut self, yes: bool) -> &mut RegexBuilder {
if yes {
self.config.jit = JITChoice::Attempt;
} else {
self.config.jit = JITChoice::Never;
}
self
}
pub fn max_jit_stack_size(
&mut self,
bytes: Option<usize>,
) -> &mut RegexBuilder {
self.config.match_config.max_jit_stack_size = bytes;
self
}
}
pub struct Regex {
config: Arc<Config>,
pattern: String,
code: Arc<Code>,
capture_names: Arc<Vec<Option<String>>>,
capture_names_idx: Arc<HashMap<String, usize>>,
match_data: MatchDataPool,
}
impl Clone for Regex {
fn clone(&self) -> Regex {
let match_data = {
let config = self.config.match_config.clone();
let code = Arc::clone(&self.code);
let create: MatchDataPoolFn =
Box::new(move || MatchData::new(config.clone(), &code));
Pool::new(create)
};
Regex {
config: Arc::clone(&self.config),
pattern: self.pattern.clone(),
code: Arc::clone(&self.code),
capture_names: Arc::clone(&self.capture_names),
capture_names_idx: Arc::clone(&self.capture_names_idx),
match_data,
}
}
}
impl std::fmt::Debug for Regex {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "Regex({:?})", self.pattern)
}
}
impl Regex {
pub fn new(pattern: &str) -> Result<Regex, Error> {
RegexBuilder::new().build(pattern)
}
pub fn is_match(&self, subject: &[u8]) -> Result<bool, Error> {
self.is_match_at(subject, 0)
}
pub fn find<'s>(
&self,
subject: &'s [u8],
) -> Result<Option<Match<'s>>, Error> {
self.find_at(subject, 0)
}
pub fn find_iter<'r, 's>(&'r self, subject: &'s [u8]) -> Matches<'r, 's> {
Matches {
re: self,
match_data: self.match_data(),
subject,
last_end: 0,
last_match: None,
}
}
pub fn captures<'s>(
&self,
subject: &'s [u8],
) -> Result<Option<Captures<'s>>, Error> {
let mut locs = self.capture_locations();
Ok(self.captures_read(&mut locs, subject)?.map(move |_| Captures {
subject,
locs,
idx: Arc::clone(&self.capture_names_idx),
}))
}
pub fn captures_iter<'r, 's>(
&'r self,
subject: &'s [u8],
) -> CaptureMatches<'r, 's> {
CaptureMatches { re: self, subject, last_end: 0, last_match: None }
}
}
impl Regex {
pub fn is_match_at(
&self,
subject: &[u8],
start: usize,
) -> Result<bool, Error> {
assert!(
start <= subject.len(),
"start ({}) must be <= subject.len() ({})",
start,
subject.len()
);
let options = 0;
let mut match_data = self.match_data();
let res =
unsafe { match_data.find(&self.code, subject, start, options) };
PoolGuard::put(match_data);
res
}
pub fn find_at<'s>(
&self,
subject: &'s [u8],
start: usize,
) -> Result<Option<Match<'s>>, Error> {
let mut match_data = self.match_data();
let res =
self.find_at_with_match_data(&mut match_data, subject, start);
PoolGuard::put(match_data);
res
}
#[inline(always)]
fn find_at_with_match_data<'s>(
&self,
match_data: &mut MatchDataPoolGuard<'_>,
subject: &'s [u8],
start: usize,
) -> Result<Option<Match<'s>>, Error> {
assert!(
start <= subject.len(),
"start ({}) must be <= subject.len() ({})",
start,
subject.len()
);
let options = 0;
if unsafe { !match_data.find(&self.code, subject, start, options)? } {
return Ok(None);
}
let ovector = match_data.ovector();
let (s, e) = (ovector[0], ovector[1]);
Ok(Some(Match::new(&subject, s, e)))
}
pub fn captures_read<'s>(
&self,
locs: &mut CaptureLocations,
subject: &'s [u8],
) -> Result<Option<Match<'s>>, Error> {
self.captures_read_at(locs, subject, 0)
}
pub fn captures_read_at<'s>(
&self,
locs: &mut CaptureLocations,
subject: &'s [u8],
start: usize,
) -> Result<Option<Match<'s>>, Error> {
assert!(
start <= subject.len(),
"start ({}) must be <= subject.len() ({})",
start,
subject.len()
);
let options = 0;
if unsafe { !locs.data.find(&self.code, subject, start, options)? } {
return Ok(None);
}
let ovector = locs.data.ovector();
let (s, e) = (ovector[0], ovector[1]);
Ok(Some(Match::new(&subject, s, e)))
}
}
impl Regex {
pub fn as_str(&self) -> &str {
&self.pattern
}
pub fn capture_names(&self) -> &[Option<String>] {
&self.capture_names
}
pub fn captures_len(&self) -> usize {
self.code.capture_count().expect("a valid capture count from PCRE2")
}
pub fn capture_locations(&self) -> CaptureLocations {
CaptureLocations {
code: Arc::clone(&self.code),
data: self.new_match_data(),
}
}
fn match_data(&self) -> MatchDataPoolGuard<'_> {
self.match_data.get()
}
fn new_match_data(&self) -> MatchData {
MatchData::new(self.config.match_config.clone(), &self.code)
}
}
pub struct CaptureLocations {
code: Arc<Code>,
data: MatchData,
}
impl Clone for CaptureLocations {
fn clone(&self) -> CaptureLocations {
CaptureLocations {
code: Arc::clone(&self.code),
data: MatchData::new(self.data.config().clone(), &self.code),
}
}
}
impl std::fmt::Debug for CaptureLocations {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let mut offsets: Vec<Option<usize>> = vec![];
for &offset in self.data.ovector() {
if offset == PCRE2_UNSET {
offsets.push(None);
} else {
offsets.push(Some(offset));
}
}
write!(f, "CaptureLocations(")?;
f.debug_list().entries(offsets).finish()?;
write!(f, ")")
}
}
impl CaptureLocations {
#[inline]
pub fn get(&self, i: usize) -> Option<(usize, usize)> {
let start_index = i.checked_mul(2)?;
let end_index = start_index.checked_add(1)?;
let ovec = self.data.ovector();
let start = *ovec.get(start_index)?;
let end = *ovec.get(end_index)?;
if start == PCRE2_UNSET || end == PCRE2_UNSET {
return None;
}
Some((start, end))
}
#[inline]
pub fn len(&self) -> usize {
self.data.ovector().len() / 2
}
}
pub struct Captures<'s> {
subject: &'s [u8],
locs: CaptureLocations,
idx: Arc<HashMap<String, usize>>,
}
impl<'s> Captures<'s> {
pub fn get(&self, i: usize) -> Option<Match<'s>> {
self.locs.get(i).map(|(s, e)| Match::new(self.subject, s, e))
}
pub fn name(&self, name: &str) -> Option<Match<'s>> {
self.idx.get(name).and_then(|&i| self.get(i))
}
#[inline]
pub fn len(&self) -> usize {
self.locs.len()
}
}
impl<'s> std::fmt::Debug for Captures<'s> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.debug_tuple("Captures").field(&CapturesDebug(self)).finish()
}
}
struct CapturesDebug<'c, 's: 'c>(&'c Captures<'s>);
impl<'c, 's> std::fmt::Debug for CapturesDebug<'c, 's> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
fn escape_bytes(bytes: &[u8]) -> String {
let mut s = String::new();
for &b in bytes {
s.push_str(&escape_byte(b));
}
s
}
fn escape_byte(byte: u8) -> String {
use std::ascii::escape_default;
let escaped: Vec<u8> = escape_default(byte).collect();
String::from_utf8_lossy(&escaped).into_owned()
}
let slot_to_name: HashMap<&usize, &String> =
self.0.idx.iter().map(|(a, b)| (b, a)).collect();
let mut map = f.debug_map();
for slot in 0..self.0.len() {
let m = self
.0
.locs
.get(slot)
.map(|(s, e)| escape_bytes(&self.0.subject[s..e]));
if let Some(name) = slot_to_name.get(&slot) {
map.entry(&name, &m);
} else {
map.entry(&slot, &m);
}
}
map.finish()
}
}
impl<'s> std::ops::Index<usize> for Captures<'s> {
type Output = [u8];
fn index(&self, i: usize) -> &[u8] {
self.get(i)
.map(|m| m.as_bytes())
.unwrap_or_else(|| panic!("no group at index '{}'", i))
}
}
impl<'s, 'i> std::ops::Index<&'i str> for Captures<'s> {
type Output = [u8];
fn index<'a>(&'a self, name: &'i str) -> &'a [u8] {
self.name(name)
.map(|m| m.as_bytes())
.unwrap_or_else(|| panic!("no group named '{}'", name))
}
}
pub struct Matches<'r, 's> {
re: &'r Regex,
match_data: MatchDataPoolGuard<'r>,
subject: &'s [u8],
last_end: usize,
last_match: Option<usize>,
}
impl<'r, 's> Iterator for Matches<'r, 's> {
type Item = Result<Match<'s>, Error>;
fn next(&mut self) -> Option<Result<Match<'s>, Error>> {
if self.last_end > self.subject.len() {
return None;
}
let res = self.re.find_at_with_match_data(
&mut self.match_data,
self.subject,
self.last_end,
);
let m = match res {
Err(err) => return Some(Err(err)),
Ok(None) => return None,
Ok(Some(m)) => m,
};
if m.start() == m.end() {
self.last_end = m.end() + 1;
if Some(m.end()) == self.last_match {
return self.next();
}
} else {
self.last_end = m.end();
}
self.last_match = Some(m.end());
Some(Ok(m))
}
}
pub struct CaptureMatches<'r, 's> {
re: &'r Regex,
subject: &'s [u8],
last_end: usize,
last_match: Option<usize>,
}
impl<'r, 's> Iterator for CaptureMatches<'r, 's> {
type Item = Result<Captures<'s>, Error>;
fn next(&mut self) -> Option<Result<Captures<'s>, Error>> {
if self.last_end > self.subject.len() {
return None;
}
let mut locs = self.re.capture_locations();
let res =
self.re.captures_read_at(&mut locs, self.subject, self.last_end);
let m = match res {
Err(err) => return Some(Err(err)),
Ok(None) => return None,
Ok(Some(m)) => m,
};
if m.start() == m.end() {
self.last_end = m.end() + 1;
if Some(m.end()) == self.last_match {
return self.next();
}
} else {
self.last_end = m.end();
}
self.last_match = Some(m.end());
Some(Ok(Captures {
subject: self.subject,
locs,
idx: Arc::clone(&self.re.capture_names_idx),
}))
}
}
type MatchDataPool = Pool<MatchData, MatchDataPoolFn>;
type MatchDataPoolGuard<'a> = PoolGuard<'a, MatchData, MatchDataPoolFn>;
type MatchDataPoolFn =
Box<dyn Fn() -> MatchData + Send + Sync + UnwindSafe + RefUnwindSafe>;
#[cfg(test)]
mod tests {
use super::{Regex, RegexBuilder};
use crate::is_jit_available;
fn b(string: &str) -> &[u8] {
string.as_bytes()
}
fn find_iter_tuples(re: &Regex, subject: &[u8]) -> Vec<(usize, usize)> {
let mut tuples = vec![];
for result in re.find_iter(subject) {
let m = result.unwrap();
tuples.push((m.start(), m.end()));
}
tuples
}
fn cap_iter_tuples(re: &Regex, subject: &[u8]) -> Vec<(usize, usize)> {
let mut tuples = vec![];
for result in re.captures_iter(subject) {
let caps = result.unwrap();
let m = caps.get(0).unwrap();
tuples.push((m.start(), m.end()));
}
tuples
}
#[test]
fn caseless() {
let re = RegexBuilder::new().caseless(true).build("a").unwrap();
assert!(re.is_match(b("A")).unwrap());
let re =
RegexBuilder::new().caseless(true).ucp(true).build("β").unwrap();
assert!(re.is_match(b("Β")).unwrap());
}
#[test]
fn crlf() {
let re = RegexBuilder::new().crlf(true).build("a$").unwrap();
let m = re.find(b("a\r\n")).unwrap().unwrap();
assert_eq!(m.as_pair(), (0, 1));
}
#[test]
fn dotall() {
let re = RegexBuilder::new().dotall(false).build(".").unwrap();
assert!(!re.is_match(b("\n")).unwrap());
let re = RegexBuilder::new().dotall(true).build(".").unwrap();
assert!(re.is_match(b("\n")).unwrap());
}
#[test]
fn extended() {
let re = RegexBuilder::new().extended(true).build("a b c").unwrap();
assert!(re.is_match(b("abc")).unwrap());
}
#[test]
fn multi_line() {
let re = RegexBuilder::new().multi_line(false).build("^abc$").unwrap();
assert!(!re.is_match(b("foo\nabc\nbar")).unwrap());
let re = RegexBuilder::new().multi_line(true).build("^abc$").unwrap();
assert!(re.is_match(b("foo\nabc\nbar")).unwrap());
}
#[test]
fn ucp() {
let re = RegexBuilder::new().ucp(false).build(r"\w").unwrap();
assert!(!re.is_match(b("β")).unwrap());
let re = RegexBuilder::new().ucp(true).build(r"\w").unwrap();
assert!(re.is_match(b("β")).unwrap());
}
#[test]
fn utf() {
let re = RegexBuilder::new().utf(false).build(".").unwrap();
assert_eq!(re.find(b("β")).unwrap().unwrap().as_pair(), (0, 1));
let re = RegexBuilder::new().utf(true).build(".").unwrap();
assert_eq!(re.find(b("β")).unwrap().unwrap().as_pair(), (0, 2));
}
#[test]
fn jit4lyfe() {
if is_jit_available() {
let re = RegexBuilder::new().jit(true).build(r"\w").unwrap();
assert!(re.is_match(b("a")).unwrap());
} else {
RegexBuilder::new().jit(true).build(r"\w").unwrap_err();
}
}
#[test]
fn jit_if_available() {
let re =
RegexBuilder::new().jit_if_available(true).build(r"\w").unwrap();
assert!(re.is_match(b("a")).unwrap());
}
#[test]
fn jit_test_lazy_alloc_subject() {
let subject: Vec<u8> = vec![];
let re = RegexBuilder::new()
.jit_if_available(true)
.build(r"xxxx|xxxx|xxxx")
.unwrap();
assert!(!re.is_match(&subject).unwrap());
}
#[test]
fn utf_with_invalid_data() {
let re = RegexBuilder::new().build(r".").unwrap();
assert_eq!(re.find(b"\xFF").unwrap().unwrap().as_pair(), (0, 1));
let re = RegexBuilder::new().utf(true).build(r".").unwrap();
assert!(re.find(b"\xFF").is_err());
}
#[test]
fn capture_names() {
let re = RegexBuilder::new()
.build(r"(?P<foo>abc)|(def)|(?P<a>ghi)|(?P<springsteen>jkl)")
.unwrap();
assert_eq!(
re.capture_names().to_vec(),
vec![
None,
Some("foo".to_string()),
None,
Some("a".to_string()),
Some("springsteen".to_string()),
]
);
assert_eq!(re.capture_names_idx.len(), 3);
assert_eq!(re.capture_names_idx["foo"], 1);
assert_eq!(re.capture_names_idx["a"], 3);
assert_eq!(re.capture_names_idx["springsteen"], 4);
}
#[test]
fn captures_get() {
let re = Regex::new(r"[a-z]+(?:([0-9]+)|([A-Z]+))").unwrap();
let caps = re.captures(b"abc123").unwrap().unwrap();
let text1 = caps.get(1).map_or(&b""[..], |m| m.as_bytes());
let text2 = caps.get(2).map_or(&b""[..], |m| m.as_bytes());
assert_eq!(text1, &b"123"[..]);
assert_eq!(text2, &b""[..]);
assert_eq!(caps.get(usize::MAX), None);
}
#[test]
fn find_iter_empty() {
let re = Regex::new(r"(?m:^)").unwrap();
assert_eq!(find_iter_tuples(&re, b""), vec![(0, 0)]);
assert_eq!(find_iter_tuples(&re, b"\n"), vec![(0, 0)]);
assert_eq!(find_iter_tuples(&re, b"\n\n"), vec![(0, 0), (1, 1)]);
assert_eq!(find_iter_tuples(&re, b"\na\n"), vec![(0, 0), (1, 1)]);
assert_eq!(
find_iter_tuples(&re, b"\na\n\n"),
vec![(0, 0), (1, 1), (3, 3),]
);
}
#[test]
fn captures_iter_empty() {
let re = Regex::new(r"(?m:^)").unwrap();
assert_eq!(cap_iter_tuples(&re, b""), vec![(0, 0)]);
assert_eq!(cap_iter_tuples(&re, b"\n"), vec![(0, 0)]);
assert_eq!(cap_iter_tuples(&re, b"\n\n"), vec![(0, 0), (1, 1)]);
assert_eq!(cap_iter_tuples(&re, b"\na\n"), vec![(0, 0), (1, 1)]);
assert_eq!(
cap_iter_tuples(&re, b"\na\n\n"),
vec![(0, 0), (1, 1), (3, 3),]
);
}
#[test]
fn max_jit_stack_size_does_something() {
if !is_jit_available() {
return;
}
let hundred = "\
ABCDEFGHIJKLMNOPQRSTUVWXY\
ABCDEFGHIJKLMNOPQRSTUVWXY\
ABCDEFGHIJKLMNOPQRSTUVWXY\
ABCDEFGHIJKLMNOPQRSTUVWXY\
";
let hay = format!("{}", hundred.repeat(100));
let re = RegexBuilder::new()
.ucp(true)
.jit(true)
.max_jit_stack_size(Some(1))
.build(r"((((\w{10})){100}))+")
.unwrap();
let result = re.is_match(hay.as_bytes());
if result.is_ok() {
return;
}
let err = result.unwrap_err();
assert!(err.to_string().contains("JIT stack limit reached"));
let re = RegexBuilder::new()
.ucp(true)
.jit(true)
.max_jit_stack_size(Some(1 << 20))
.build(r"((((\w{10})){100}))+")
.unwrap();
assert!(re.is_match(hay.as_bytes()).unwrap());
}
#[test]
fn find_start_end_and_as_bytes() {
let hay =
"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
let pattern = r"
(?x) (?#: Allow comments and whitespace.)
[a-z] (?#: Lowercase letter.)
+ (?#: One or more times.)
";
let re = RegexBuilder::new()
.extended(true)
.utf(true)
.jit_if_available(true)
.build(pattern)
.unwrap();
let matched = re.find(hay.as_bytes()).unwrap().unwrap();
assert_eq!(matched.start(), 10);
assert_eq!(matched.end(), 10 + 26);
assert_eq!(matched.as_bytes(), b"abcdefghijklmnopqrstuvwxyz");
}
#[test]
fn find_utf_emoji_as_bytes() {
let hay = "0123456789😀👍🏼🎉abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
let pattern = r"(*UTF)
(?x) (?#: Allow comments and whitespace.)
[^\N{U+0000}-\N{U+007F}] (?#: Non-ascii code points.)
+ (?#: One or more times.)
";
let re = RegexBuilder::new()
.extended(true)
.utf(true)
.jit_if_available(true)
.build(pattern)
.unwrap();
let matched = re.find(hay.as_bytes()).unwrap().unwrap();
assert_eq!(matched.as_bytes(), "😀👍🏼🎉".as_bytes());
}
#[test]
fn capture_get_does_not_panic() {
let re = Regex::new("").unwrap();
let caps = re.captures(b"abc").unwrap().unwrap();
assert_eq!(Some((0, 0)), caps.get(0).map(|m| (m.start(), m.end())));
assert_eq!(None, caps.get(1));
assert_eq!(None, caps.get(usize::MAX - 1));
assert_eq!(None, caps.get(usize::MAX));
}
}