use ferroni::api::{Regex, RegexBuilder};
use ferroni::error::RegexError;
use ferroni::oniguruma::ONIGERR_INVALID_BACKREF;
use ferroni::prelude::*;
use ferroni::regint::DEFAULT_PARSE_DEPTH_LIMIT;
#[test]
fn recursive_capture_above_bitset_width() {
let pattern = format!(r"{}(a\g<32>?b)", "()".repeat(31));
let regex = Regex::new(&pattern).unwrap();
assert_eq!(regex.find("aaabbb").unwrap().as_str(), "aaabbb");
assert!(!regex.is_match("aaa"));
}
#[test]
fn simple_pattern() {
let re = Regex::new(r"\d+").unwrap();
let m = re.find("abc 123 def").unwrap();
assert_eq!(m.as_str(), "123");
}
#[test]
fn unicode_pattern() {
let re = Regex::new(r"\p{Hiragana}+").unwrap();
let m = re.find("hello せかい world").unwrap();
assert_eq!(m.as_str(), "せかい");
}
#[test]
fn no_match_returns_none() {
let re = Regex::new(r"xyz").unwrap();
assert!(re.find("abc").is_none());
}
#[test]
fn empty_pattern() {
let re = Regex::new(r"").unwrap();
let m = re.find("hello").unwrap();
assert_eq!(m.start(), 0);
assert_eq!(m.end(), 0);
assert!(m.is_empty());
}
#[test]
fn invalid_pattern_syntax_error() {
let err = Regex::new(r"(unclosed").unwrap_err();
match err {
RegexError::Syntax { code, .. } => assert!(code < 0),
other => panic!("expected Syntax error, got {:?}", other),
}
}
#[test]
fn invalid_pattern_empty_char_class() {
let err = Regex::new(r"[]").unwrap_err();
assert!(matches!(err, RegexError::Syntax { .. }));
}
#[test]
fn flat_patterns_exceeding_ast_depth_limit_fail_cleanly() {
const HUGE_FLAT_PATTERN_ELEMENT_COUNT: usize = 50_000;
let alternatives = format!("{}a", "a|".repeat(HUGE_FLAT_PATTERN_ELEMENT_COUNT - 1));
let concatenation = ".".repeat(HUGE_FLAT_PATTERN_ELEMENT_COUNT);
for pattern in [&alternatives, &concatenation] {
let err = Regex::new(pattern).unwrap_err();
assert_eq!(err, RegexError::ParseDepthLimitOver);
}
}
#[test]
fn nested_wide_patterns_exceeding_ast_depth_limit_fail_cleanly() {
const NESTED_GROUP_COUNT: usize = 20;
const WIDE_ALTERNATIVE_COUNT: usize = 4_000;
const LONG_CONCATENATION_COUNT: usize = 4_000;
let mut pattern = ".".repeat(LONG_CONCATENATION_COUNT);
for _ in 0..NESTED_GROUP_COUNT {
pattern = format!("(?:{}{})", "a|".repeat(WIDE_ALTERNATIVE_COUNT), pattern);
}
let err = Regex::new(&pattern).unwrap_err();
assert_eq!(err, RegexError::ParseDepthLimitOver);
}
#[test]
fn long_literal_larger_than_ast_depth_limit_is_supported() {
let pattern = "a".repeat(DEFAULT_PARSE_DEPTH_LIMIT as usize * 2);
let re = Regex::new(&pattern).unwrap();
assert!(re.is_match(&pattern));
}
#[test]
fn is_match_true() {
let re = Regex::new(r"world").unwrap();
assert!(re.is_match("hello world"));
}
#[test]
fn is_match_false() {
let re = Regex::new(r"world").unwrap();
assert!(!re.is_match("hello earth"));
}
#[test]
fn find_start_end_range() {
let re = Regex::new(r"bar").unwrap();
let m = re.find("foobarbaz").unwrap();
assert_eq!(m.start(), 3);
assert_eq!(m.end(), 6);
assert_eq!(m.range(), 3..6);
assert_eq!(m.len(), 3);
assert!(!m.is_empty());
}
#[test]
fn find_as_bytes() {
let re = Regex::new(r"\w+").unwrap();
let m = re.find("hello world").unwrap();
assert_eq!(m.as_bytes(), b"hello");
}
#[test]
fn unset_backreference_does_not_reuse_prior_capture() {
let prior = Regex::new(r"(abc)(def)").unwrap();
assert!(prior.captures("abcdef").is_some());
let re = Regex::new(r"(?:(foo)|bar)\1").unwrap();
assert!(re.find("barbar").is_none());
}
#[test]
fn captures_groups() {
let re = Regex::new(r"(\w+)\s+(\w+)").unwrap();
let caps = re.captures("hello world").unwrap();
assert_eq!(caps.get(0).unwrap().as_str(), "hello world");
assert_eq!(caps.get(1).unwrap().as_str(), "hello");
assert_eq!(caps.get(2).unwrap().as_str(), "world");
assert_eq!(caps.len(), 3); }
#[test]
fn captures_optional_group() {
let re = Regex::new(r"(a)(b)?c").unwrap();
let caps = re.captures("ac").unwrap();
assert_eq!(caps.get(0).unwrap().as_str(), "ac");
assert_eq!(caps.get(1).unwrap().as_str(), "a");
assert!(caps.get(2).is_none()); }
#[test]
fn captures_optional_group_is_not_reused_from_prior_regex() {
let prior = Regex::new(r"(aaa)(bbb)(ccc)").unwrap();
assert!(prior.captures("aaabbbccc").is_some());
let re = Regex::new(r"(x)?z").unwrap();
let caps = re.captures("z").unwrap();
assert!(caps.get(1).is_none());
}
#[test]
fn captures_named() {
let re = Regex::new(r"(?<first>\w+)\s+(?<last>\w+)").unwrap();
let caps = re.captures("John Doe").unwrap();
assert_eq!(caps.name("first").unwrap().as_str(), "John");
assert_eq!(caps.name("last").unwrap().as_str(), "Doe");
assert!(caps.name("middle").is_none());
}
#[test]
fn captures_multiplex_name_returns_last_participating_group() {
let re = Regex::new(r"(?<letter>a)(?<letter>b)").unwrap();
let caps = re.captures("ab").unwrap();
assert_eq!(caps.name("letter").unwrap().as_str(), "b");
}
#[test]
fn captures_multiplex_name_returns_earlier_participating_group() {
let re = Regex::new(r"(?:(?<letter>a)|(?<letter>b))").unwrap();
let caps = re.captures("a").unwrap();
assert_eq!(caps.name("letter").unwrap().as_str(), "a");
}
#[test]
fn captures_multiplex_name_returns_later_participating_group() {
let re = Regex::new(r"(?:(?<letter>a)|(?<letter>b))").unwrap();
let caps = re.captures("b").unwrap();
assert_eq!(caps.name("letter").unwrap().as_str(), "b");
}
#[test]
fn captures_multiplex_name_returns_none_when_no_group_participates() {
let re = Regex::new(r"(?:(?<letter>a)|(?<letter>b))?").unwrap();
let caps = re.captures("").unwrap();
assert!(caps.name("letter").is_none());
}
#[test]
fn captures_name_returns_unique_named_group() {
let re = Regex::new(r"(?<letter>a)").unwrap();
let caps = re.captures("a").unwrap();
assert_eq!(caps.name("letter").unwrap().as_str(), "a");
}
#[test]
fn captures_no_match() {
let re = Regex::new(r"(\d+)").unwrap();
assert!(re.captures("no digits").is_none());
}
#[test]
fn captures_len() {
let re = Regex::new(r"(a)(b)(c)(d)").unwrap();
assert_eq!(re.captures_len(), 4);
}
#[test]
fn captures_iter() {
let re = Regex::new(r"(a)(b)").unwrap();
let caps = re.captures("ab").unwrap();
let items: Vec<_> = caps.iter().collect();
assert_eq!(items.len(), 3);
assert_eq!(items[0].unwrap().as_str(), "ab");
assert_eq!(items[1].unwrap().as_str(), "a");
assert_eq!(items[2].unwrap().as_str(), "b");
}
#[test]
fn find_iter_multiple() {
let re = Regex::new(r"\d+").unwrap();
let results: Vec<&str> = re
.find_iter("1 and 22 and 333")
.map(|m| m.as_str())
.collect();
assert_eq!(results, vec!["1", "22", "333"]);
}
#[test]
fn find_iter_no_matches() {
let re = Regex::new(r"\d+").unwrap();
let results: Vec<_> = re.find_iter("no digits").collect();
assert!(results.is_empty());
}
#[test]
fn find_iter_empty_pattern() {
let re = Regex::new(r"").unwrap();
let results: Vec<_> = re.find_iter("ab").collect();
assert_eq!(results.len(), 3);
for (i, m) in results.iter().enumerate() {
assert_eq!(m.start(), i);
assert!(m.is_empty());
}
}
#[test]
fn find_iter_overlapping_region() {
let re = Regex::new(r"\w+").unwrap();
let results: Vec<&str> = re.find_iter("a bb ccc").map(|m| m.as_str()).collect();
assert_eq!(results, vec!["a", "bb", "ccc"]);
}
#[test]
fn builder_case_insensitive() {
let re = RegexBuilder::new(r"hello")
.case_insensitive(true)
.build()
.unwrap();
assert!(re.is_match("HELLO"));
assert!(re.is_match("HeLlO"));
}
#[test]
fn builder_dot_matches_newline() {
let re = Regex::builder(r"a.b")
.dot_matches_newline(true)
.build()
.unwrap();
assert!(re.is_match("a\nb"));
let re2 = Regex::builder(r"a.b").build().unwrap();
assert!(!re2.is_match("a\nb"));
}
#[test]
fn builder_extended_mode() {
let re = Regex::builder(
r"
\d+ # digits
\s+ # space
\w+ # word
",
)
.extended(true)
.build()
.unwrap();
assert!(re.is_match("42 hello"));
}
#[test]
fn builder_syntax() {
use ferroni::regsyntax::OnigSyntaxPerl;
let re = Regex::builder(r"\d+")
.syntax(&OnigSyntaxPerl)
.build()
.unwrap();
assert!(re.is_match("42"));
}
#[test]
fn prototype_ecmascript_negative_lookbehind_is_opt_in() {
use ferroni::regsyntax::prototype_ecmascript_lookbehind_syntax;
assert!(Regex::new(r"(?<!(a))b").is_err());
assert!(Regex::new(r"(?<!a(?=b))b").is_err());
let capture = Regex::builder(r"(?<!(a+|bc))d")
.syntax(prototype_ecmascript_lookbehind_syntax())
.build()
.unwrap();
assert!(capture.is_match("d"));
assert!(!capture.is_match("ad"));
assert!(!capture.is_match("bcd"));
let positive = Regex::builder(r"(?<!a(?=b))b")
.syntax(prototype_ecmascript_lookbehind_syntax())
.build()
.unwrap();
assert!(positive.is_match("b"));
assert!(!positive.is_match("ab"));
let negative = Regex::builder(r"(?<!a(?!c))b")
.syntax(prototype_ecmascript_lookbehind_syntax())
.build()
.unwrap();
assert!(!negative.is_match("ab"));
assert!(negative.is_match("acb"));
}
#[test]
fn prototype_ecmascript_unmatched_backreferences_are_empty() {
use ferroni::regsyntax::prototype_ecmascript_lookbehind_syntax;
for pattern in [r"(a)?b\1", r"(a)(b)?c\2", r"(a)(b)(c)?d\3"] {
let re = Regex::builder(pattern)
.syntax(prototype_ecmascript_lookbehind_syntax())
.build()
.unwrap();
assert!(re.is_match(match pattern {
r"(a)?b\1" => "b",
r"(a)(b)?c\2" => "ac",
_ => "abd",
}));
}
let insensitive = Regex::builder(r"(a)(b)(c)?d\3")
.syntax(prototype_ecmascript_lookbehind_syntax())
.case_insensitive(true)
.build()
.unwrap();
assert!(insensitive.is_match("ABD"));
let named = Regex::builder(r"(?:(?<x>a)|(?<x>b))?c\k<x>")
.syntax(prototype_ecmascript_lookbehind_syntax())
.build()
.unwrap();
assert!(named.is_match("c"));
let named_insensitive = Regex::builder(r"(?:(?<x>a)|(?<x>b))?c\k<x>")
.syntax(prototype_ecmascript_lookbehind_syntax())
.case_insensitive(true)
.build()
.unwrap();
assert!(named_insensitive.is_match("C"));
}
#[test]
fn builder_chaining() {
let re = Regex::builder(r"hello world")
.case_insensitive(true)
.dot_matches_newline(true)
.extended(false)
.build()
.unwrap();
assert!(re.is_match("HELLO WORLD"));
}
#[test]
fn find_bytes() {
let re = Regex::new_bytes(b"\\d+").unwrap();
let m = re.find_bytes(b"abc 42 def").unwrap();
assert_eq!(m.as_bytes(), b"42");
assert_eq!(m.start(), 4);
}
#[test]
fn is_match_bytes() {
let re = Regex::new_bytes(b"hello").unwrap();
assert!(re.is_match_bytes(b"say hello"));
assert!(!re.is_match_bytes(b"goodbye"));
}
#[test]
fn byte_matching_clamps_truncated_utf8_word_steps() {
let cases: &[(&str, &[u8])] = &[
(r"\w+", b"hello\xf0\x9f\x92"),
(r"\b\w+\b", b"\xf0\x9f"),
(r"\w\b", b"\xf0\x9f"),
(r"\w+", b"abc\xc2"),
(r"(?W)\W", b"\xf0"),
];
for &(pattern, input) in cases {
let re = Regex::new(pattern).unwrap();
assert!(re.is_match_bytes(input), "pattern {pattern:?}");
let matched = re.find_bytes(input).expect("matching input");
assert_eq!(matched.range(), 0..input.len(), "pattern {pattern:?}");
assert_eq!(matched.as_bytes(), input, "pattern {pattern:?}");
}
}
#[test]
fn byte_matching_clamps_truncated_utf8_negated_class_steps() {
let input = b"\xf0";
let re = Regex::new_bytes(b"([^a]+)").unwrap();
let matched = re.find_bytes(input).expect("matching input");
assert_eq!(matched.range(), 0..input.len());
assert_eq!(matched.as_bytes(), input);
let captures = re.captures_bytes(input).expect("matching input");
for index in 0..captures.len() {
let capture = captures.get(index).expect("participating capture");
assert_eq!(capture.range(), 0..input.len());
assert_eq!(capture.as_bytes(), input);
}
}
#[test]
fn error_display() {
let err = Regex::new(r"(").unwrap_err();
let msg = format!("{}", err);
assert!(!msg.is_empty());
}
#[test]
fn error_is_std_error() {
let err: Box<dyn std::error::Error> = Box::new(Regex::new(r"[").unwrap_err());
assert!(!err.to_string().is_empty());
}
#[test]
fn error_code() {
let err = Regex::new(r"(").unwrap_err();
assert!(err.code() < 0);
}
#[test]
fn prelude_imports_work() {
let re = Regex::new(r"(\w+)").unwrap();
let caps: Captures = re.captures("hello").unwrap();
let m: Match = caps.get(0).unwrap();
assert_eq!(m.as_str(), "hello");
let _: &RegexError = &Regex::new(r"(").unwrap_err();
}
#[test]
fn as_raw_access() {
use ferroni::regexec::onig_number_of_captures;
let re = Regex::new(r"(a)(b)(c)").unwrap();
let raw = re.as_raw();
assert_eq!(onig_number_of_captures(raw), 3);
}
#[test]
fn alternation() {
let re = Regex::new(r"cat|dog|bird").unwrap();
assert_eq!(re.find("I have a dog").unwrap().as_str(), "dog");
}
#[test]
fn backreference() {
let re = Regex::new(r"(\w+)\s+\1").unwrap();
let m = re.find("hello hello world").unwrap();
assert_eq!(m.as_str(), "hello hello");
}
#[test]
fn numbered_backreferences_validate_capture_group_bounds() {
for pattern in [
b"\\1".as_slice(),
b"\\8",
b"\\9",
b"(a)\\2",
b"\\k<2>",
b"\\k<8>",
b"\\k<9>",
b"\\k<80>",
b"(a)\\k<2>",
] {
let err = Regex::new_bytes(pattern).unwrap_err();
assert_eq!(err.code(), ONIGERR_INVALID_BACKREF, "pattern: {pattern:?}");
}
}
#[test]
fn group_reference_numbers_reject_overflow_and_missing_terminators() {
for pattern in [
br"(a)\k<a+999999999999999>".as_slice(),
br"(?(9999999999999)a|b)".as_slice(),
br"(a)(?(1+9999999999999)a|b)".as_slice(),
br"(a)\k<1".as_slice(),
br"(a)\g<1".as_slice(),
br"(a)\k'1".as_slice(),
] {
assert!(Regex::new_bytes(pattern).is_err(), "{pattern:?}");
}
}
#[test]
fn valid_numbered_backreferences_compile_and_match() {
for (pattern, input) in [
(b"(a)\\1".as_slice(), "aa"),
(b"(a)\\k<1>".as_slice(), "aa"),
(b"(a)(b)(c)(d)(e)(f)(g)(h)\\8".as_slice(), "abcdefghh"),
(b"(a)(b)(c)(d)(e)(f)(g)(h)\\k<8>".as_slice(), "abcdefghh"),
] {
let re = Regex::new_bytes(pattern).unwrap();
assert!(re.is_match(input), "pattern: {pattern:?}, input: {input:?}");
}
}
#[test]
fn lookahead() {
let re = Regex::new(r"\d+(?= dollars)").unwrap();
let m = re.find("I have 42 dollars").unwrap();
assert_eq!(m.as_str(), "42");
}
#[test]
fn lookbehind() {
let re = Regex::new(r"(?<=\$)\d+").unwrap();
let m = re.find("price: $99").unwrap();
assert_eq!(m.as_str(), "99");
}
#[test]
fn possessive_quantifier() {
let re = Regex::new(r"a++b").unwrap();
assert!(re.is_match("aab"));
assert!(!re.is_match("aa"));
}
#[test]
fn date_extraction() {
let re = Regex::new(r"(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})").unwrap();
let caps = re.captures("Today is 2026-02-14.").unwrap();
assert_eq!(caps.name("year").unwrap().as_str(), "2026");
assert_eq!(caps.name("month").unwrap().as_str(), "02");
assert_eq!(caps.name("day").unwrap().as_str(), "14");
assert_eq!(caps.get(0).unwrap().as_str(), "2026-02-14");
}
#[test]
fn debug_impl() {
let re = Regex::new(r"\d+").unwrap();
let dbg = format!("{:?}", re);
assert!(dbg.contains("Regex"));
}
#[test]
fn captures_debug_impl() {
let re = Regex::new(r"(\d+)").unwrap();
let caps = re.captures("42").unwrap();
let dbg = format!("{:?}", caps);
assert!(!dbg.is_empty());
}
#[test]
fn case_insensitive_unicode_latin() {
let re = Regex::builder(r"straße")
.case_insensitive(true)
.build()
.unwrap();
assert!(re.is_match("STRASSE"));
assert!(re.is_match("Straße"));
}
#[test]
fn case_insensitive_latin1_simple_fold_uses_multibyte_class() {
let re = Regex::new(r"(?i)ö").unwrap();
assert!(re.is_match("ö"));
assert!(re.is_match("Ö"));
assert!(!re.is_match_bytes(&[0xd6, 0x96])); }
#[test]
fn case_insensitive_unicode_greek() {
let re = Regex::builder(r"σ").case_insensitive(true).build().unwrap();
assert!(re.is_match("Σ")); assert!(re.is_match("ς")); }
#[test]
fn case_insensitive_multi_char_fold() {
let re = Regex::builder(r"ffi")
.case_insensitive(true)
.build()
.unwrap();
assert!(re.is_match("ffi"));
assert!(re.is_match("FFI"));
}
#[test]
fn case_insensitive_char_class_unicode() {
let re = Regex::builder(r"[äöü]+")
.case_insensitive(true)
.build()
.unwrap();
assert!(re.is_match("ÄÖÜ"));
assert!(re.is_match("äöü"));
}
#[test]
fn word_star_non_ascii() {
let re = Regex::new(r"\w*").unwrap();
let m = re.find("café").unwrap();
assert_eq!(m.as_str(), "café");
}
#[test]
fn word_star_peek_next() {
let re = Regex::new(r"\w*:").unwrap();
let m = re.find("key: value").unwrap();
assert_eq!(m.as_str(), "key:");
}
#[test]
fn word_boundary_unicode() {
let re = Regex::new(r"\b\w+\b").unwrap();
let results: Vec<&str> = re
.find_iter("hello·world café")
.map(|m| m.as_str())
.collect();
assert!(results.contains(&"hello"));
assert!(results.contains(&"café"));
}
#[test]
fn subexpression_call_in_quantifier() {
let re = Regex::new(r"(?<digit>\d)(\g<digit>)+").unwrap();
assert!(re.is_match("123"));
}
#[test]
fn subexpression_call_in_lookahead() {
let re = Regex::new(r"(?<d>\d)(?=\g<d>)\d").unwrap();
assert!(re.is_match("11"));
assert!(re.is_match("12")); }
#[test]
fn subexpression_call_in_negative_lookahead() {
let re = Regex::new(r"(?<d>\d)(?!\g<d>)x").unwrap();
assert!(re.is_match("1x")); assert!(!re.is_match("12")); }
#[test]
fn subexpression_call_in_lookbehind() {
let re = Regex::new(r"(?<d>\d)(?<=\g<d>)\w").unwrap();
assert!(re.is_match("1a"));
}
#[test]
fn if_else_conditional() {
let re = Regex::new(r"^(a)?(?(1)b|c)$").unwrap();
assert!(re.is_match("ab"));
assert!(re.is_match("c"));
}
#[test]
fn backref_with_level() {
let re = Regex::new(r"\A(?<a>|.|(?:(?<b>.)\g<a>\k<b+0>))\z").unwrap();
assert!(re.is_match("a"));
assert!(re.is_match("aba"));
}
#[test]
fn begin_line_anchor_multiline() {
let re = Regex::new(r"^\w+").unwrap();
let results: Vec<&str> = re
.find_iter("hello\nworld\nfoo")
.map(|m| m.as_str())
.collect();
assert!(!results.is_empty());
}
#[test]
fn end_line_anchor() {
let re = Regex::new(r"\w+$").unwrap();
let m = re.find("hello world").unwrap();
assert_eq!(m.as_str(), "world");
}
#[test]
fn multiline_anchors() {
let re = Regex::builder(r"^\w+$")
.dot_matches_newline(false)
.build()
.unwrap();
let m = re.find("hello").unwrap();
assert_eq!(m.as_str(), "hello");
}
#[test]
fn callout_max_count() {
let re = Regex::new(r"(?:(*MAX{5})a)*").unwrap();
let m = re.find("aaaaaa").unwrap();
assert!(m.len() <= 6);
}
#[test]
fn callout_count() {
let re = Regex::new(r"(?:(*COUNT)a)+").unwrap();
assert!(re.is_match("aaa"));
}
#[test]
fn possessive_repeat() {
let re = Regex::new(r"[a-z]++").unwrap();
assert!(re.is_match("abc"));
}
#[test]
fn named_group_multiple_alternation() {
let re = Regex::new(r"(?<x>a)|(?<x>b)").unwrap();
assert!(re.is_match("a"));
assert!(re.is_match("b"));
}
#[test]
fn unicode_property_negated() {
let re = Regex::new(r"\P{Lu}+").unwrap();
let m = re.find("ABCdef").unwrap();
assert_eq!(m.as_str(), "def");
}
#[test]
fn extended_grapheme_cluster() {
let re = Regex::new(r"\X").unwrap();
let m = re.find("🇩🇪x").unwrap();
assert!(m.len() > 1); }
#[test]
fn word_boundary_types() {
let re = Regex::new(r"\b\w+\b").unwrap();
assert!(re.is_match("カタカナ"));
let m = re.find("abc123def").unwrap();
assert_eq!(m.as_str(), "abc123def");
}
#[test]
fn absent_expression() {
let re = Regex::new(r"(?~abc)").unwrap();
let m = re.find("xxabcyy").unwrap();
assert_eq!(m.as_str(), "xx");
}
#[test]
fn nested_repeat_with_capture() {
let re = Regex::new(r"((a){2,4}){1,3}").unwrap();
let m = re.find("aaaa").unwrap();
assert_eq!(m.as_str(), "aaaa");
}
#[test]
fn character_class_intersection() {
let re = Regex::new(r"[a-z&&[^aeiou]]+").unwrap();
let m = re.find("abcde").unwrap();
assert_eq!(m.as_str(), "bcd");
}
#[test]
fn hex_escape_unicode() {
let re = Regex::new(r"\x{1F600}").unwrap();
assert!(re.is_match("😀"));
}
#[test]
fn keep_marker() {
let re = Regex::new(r"foo\Kbar").unwrap();
let m = re.find("foobar").unwrap();
assert_eq!(m.as_str(), "bar");
}
#[test]
fn atomic_group() {
let re = Regex::new(r"(?>abc|ab)c").unwrap();
assert!(!re.is_match("abc"));
assert!(re.is_match("abcc"));
}
#[test]
fn lazy_quantifier_in_capture() {
let re = Regex::new(r"(a+?)(a+)").unwrap();
let caps = re.captures("aaaa").unwrap();
assert_eq!(caps.get(1).unwrap().as_str(), "a");
assert_eq!(caps.get(2).unwrap().as_str(), "aaa");
}
#[test]
fn char_class_posix() {
let re = Regex::new(r"[[:upper:]]+").unwrap();
let m = re.find("abcDEF").unwrap();
assert_eq!(m.as_str(), "DEF");
}
#[test]
fn non_greedy_repeat() {
let re = Regex::new(r"<.+?>").unwrap();
let m = re.find("<a><b>").unwrap();
assert_eq!(m.as_str(), "<a>");
}
#[test]
fn quote_literal_syntax() {
use ferroni::regsyntax::OnigSyntaxPerl;
let re = Regex::builder(r"\Q.+*\E")
.syntax(&OnigSyntaxPerl)
.build()
.unwrap();
assert!(re.is_match(".+*"));
assert!(!re.is_match("abc"));
}
#[test]
fn char_class_dash_at_end() {
let re = Regex::new(r"[a-z-]+").unwrap();
assert!(re.is_match("abc-"));
let m = re.find("ABC-def").unwrap();
assert_eq!(m.as_str(), "-def");
}
#[test]
fn char_class_range_and_dash() {
let re = Regex::new(r"[0-9-a]+").unwrap();
assert!(re.is_match("5-a"));
}
#[test]
fn named_backref_with_level() {
let re = Regex::new(r"(?<a>x)\g<a>\k<a+0>").unwrap();
assert!(re.is_match("xxx"));
}
#[test]
fn numbered_backref_with_level() {
let re = Regex::new(r"(x)\g<1>\k<1+0>").unwrap();
assert!(re.is_match("xxx"));
}
#[test]
fn char_class_negated_intersection() {
let re = Regex::new(r"[^a-z&&[^m-z]]+").unwrap();
assert!(re.is_match("M"));
}
#[test]
fn control_escape_sequences() {
let re = Regex::new(r"\ca").unwrap(); assert!(re.is_match("\x01"));
}
#[test]
fn hex_escape_2digit() {
let re = Regex::new(r"\x41").unwrap(); assert!(re.is_match("A"));
}
#[test]
fn octal_escape() {
let re = Regex::new(r"\101").unwrap(); assert!(re.is_match("A"));
}
#[test]
fn backref_with_negative_level() {
let re = Regex::new(r"(?<a>(?<b>x)\g<a>|\w)\k<b-0>");
if let Ok(re) = re {
let _ = re.is_match("xyx");
}
}
#[test]
fn unicode_named_property() {
let re = Regex::new(r"\p{Katakana}+").unwrap();
assert!(re.is_match("カタカナ"));
let re = Regex::new(r"\p{Han}+").unwrap();
assert!(re.is_match("漢字"));
}
#[test]
fn absent_clear_expression() {
let re = Regex::new(r"(?~|abc|[a-z]+)");
if let Ok(re) = re {
let _ = re.find("xyzabcdef");
}
}
#[test]
fn alternation_many_branches() {
let re = Regex::new(r"apple|banana|cherry|date|elderberry|fig|grape").unwrap();
assert_eq!(re.find("I like banana").unwrap().as_str(), "banana");
}
#[test]
fn nested_quantifier() {
let re = Regex::new(r"(a{2,3}){2,4}").unwrap();
assert!(re.is_match("aaaaaa"));
}
#[test]
fn complex_lookbehind() {
let re = Regex::new(r"(?<=ab|cd)\w+").unwrap();
assert_eq!(re.find("cdHello").unwrap().as_str(), "Hello");
}
#[test]
fn case_insensitive_backref() {
let re = Regex::builder(r"(\w+)\s+\1")
.case_insensitive(true)
.build()
.unwrap();
assert!(re.is_match("Hello HELLO"));
}
#[test]
fn many_capture_groups() {
let re = Regex::new(r"(a)(b)(c)(d)(e)(f)(g)(h)(i)(j)").unwrap();
let caps = re.captures("abcdefghij").unwrap();
assert_eq!(caps.len(), 11); assert_eq!(caps.get(10).unwrap().as_str(), "j");
}
#[test]
fn forward_reference() {
let re = Regex::new(r"(\2?(a))+").unwrap();
assert!(re.is_match("aaa"));
}
#[test]
fn match_at_end_of_string() {
let re = Regex::new(r"\w+\z").unwrap();
let m = re.find("hello world").unwrap();
assert_eq!(m.as_str(), "world");
}
#[test]
fn empty_match_progression() {
let re = Regex::new(r"(?:)+").unwrap();
let results: Vec<_> = re.find_iter("ab").collect();
assert!(!results.is_empty());
}
#[test]
fn long_alternation_literal() {
let re = Regex::new(r"cat|car|cap|can|cam|cab").unwrap();
assert_eq!(re.find("a cab ride").unwrap().as_str(), "cab");
}
#[test]
fn unicode_case_fold_in_char_class_range() {
let re = Regex::builder(r"[a-ÿ]+")
.case_insensitive(true)
.build()
.unwrap();
assert!(re.is_match("Ä"));
assert!(re.is_match("ß"));
}
#[test]
fn word_boundary_at_boundaries() {
let re = Regex::new(r"\bword\b").unwrap();
assert!(re.is_match("word"));
assert!(re.is_match("a word b"));
assert!(!re.is_match("sword"));
assert!(!re.is_match("wordy"));
}
#[test]
fn dotall_mode() {
let re = Regex::builder(r"a.b")
.dot_matches_newline(true)
.build()
.unwrap();
assert!(re.is_match("a\nb"));
assert!(re.is_match("a\rb"));
}
#[test]
fn captures_iter_with_optional_groups() {
let re = Regex::new(r"(?:(a)|(b))+").unwrap();
let caps = re.captures("abba").unwrap();
assert!(caps.get(0).is_some());
}
#[test]
fn egcb_flag_emoji_sequence() {
let re = Regex::new(r"\X+").unwrap();
let m = re.find("🇩🇪🇫🇷x").unwrap();
assert!(m.len() > 4);
}
#[test]
fn egcb_zwj_sequence() {
let re = Regex::new(r"\X").unwrap();
let m = re.find("👨\u{200D}👩\u{200D}👧x").unwrap();
assert!(m.len() > 4);
}
#[test]
fn word_boundary_hebrew() {
let re = Regex::new(r"\b\w+\b").unwrap();
assert!(re.is_match("שלום")); }
#[test]
fn word_boundary_regional_indicators() {
let re = Regex::new(r"\b.+\b").unwrap();
let _ = re.find("🇩🇪🇫🇷"); }
#[test]
fn word_boundary_extend_num_let() {
let re = Regex::new(r"\b\w+\b").unwrap();
let m = re.find("hello_world").unwrap();
assert_eq!(m.as_str(), "hello_world"); }
#[test]
fn case_insensitive_2char_fold() {
let re = Regex::builder("fi").case_insensitive(true).build().unwrap();
assert!(re.is_match("fi"));
assert!(re.is_match("FI"));
}
#[test]
fn case_insensitive_3char_fold() {
let re = Regex::builder("ffi")
.case_insensitive(true)
.build()
.unwrap();
assert!(re.is_match("ffi"));
assert!(re.is_match("FFI"));
}
#[test]
fn case_insensitive_eszett() {
let re = Regex::builder("ss").case_insensitive(true).build().unwrap();
assert!(re.is_match("ss"));
assert!(re.is_match("SS"));
assert!(re.is_match("ß"));
}
#[test]
fn case_insensitive_char_class_with_unicode_folds() {
let re = Regex::builder("[ß]")
.case_insensitive(true)
.build()
.unwrap();
assert!(re.is_match("ß"));
assert!(re.is_match("SS")); }
#[test]
fn case_insensitive_turkish_dotted_i() {
let re = Regex::builder("i").case_insensitive(true).build().unwrap();
assert!(re.is_match("I"));
assert!(re.is_match("i"));
}
#[test]
fn case_insensitive_full_width() {
let re = Regex::builder("\u{FF21}")
.case_insensitive(true)
.build()
.unwrap();
assert!(re.is_match("\u{FF21}"));
assert!(re.is_match("\u{FF41}"));
}
#[test]
fn egcb_regional_indicator_pairs() {
let re = Regex::new(r"\X").unwrap();
let input = "🇩🇪🇫🇷🇯🇵";
let clusters: Vec<_> = re.find_iter(input).collect();
assert_eq!(clusters.len(), 3);
}
#[test]
fn wb_midletter_between_letters() {
let re = Regex::new(r"\b\w+\b").unwrap();
let m = re.find("it's").unwrap();
assert!(!m.as_str().is_empty());
}
#[test]
fn wb_numeric_sequences() {
let re = Regex::new(r"\b[\w.]+\b").unwrap();
assert!(re.is_match("3.14"));
}
#[test]
fn wb_katakana_extend_num_let() {
let re = Regex::new(r"\b\w+\b").unwrap();
let m = re.find("カタカナ_テスト").unwrap();
assert_eq!(m.as_str(), "カタカナ_テスト");
}
#[test]
fn perl_named_call_syntax() {
use ferroni::regsyntax::OnigSyntaxPerl_NG;
let re = Regex::builder(r"(?<d>\d+)x(?&d)")
.syntax(&OnigSyntaxPerl_NG)
.build()
.unwrap();
assert!(re.is_match("123x456"));
assert!(!re.is_match("123xabc"));
}
#[test]
fn perl_numbered_subexp_call() {
use ferroni::regsyntax::OnigSyntaxPerl_NG;
let re = Regex::builder(r"(\d+)-(?1)")
.syntax(&OnigSyntaxPerl_NG)
.build()
.unwrap();
assert!(re.is_match("123-456"));
}
#[test]
fn perl_relative_subexp_call() {
use ferroni::regsyntax::OnigSyntaxPerl_NG;
let re = Regex::builder(r"(\d+)-(?-1)")
.syntax(&OnigSyntaxPerl_NG)
.build()
.unwrap();
assert!(re.is_match("42-99"));
}
#[test]
fn ruby_meta_escape() {
use ferroni::regsyntax::OnigSyntaxRuby;
let re = Regex::builder(r"\M-a")
.syntax(&OnigSyntaxRuby)
.build()
.unwrap();
assert!(re.is_match_bytes(&[0xc3, 0xa1])); }
#[test]
fn ruby_control_escape_c_bar() {
use ferroni::regsyntax::OnigSyntaxRuby;
let re = Regex::builder(r"\C-a")
.syntax(&OnigSyntaxRuby)
.build()
.unwrap();
assert!(re.is_match_bytes(&[0x01]));
}
#[test]
fn variable_length_lookbehind() {
let re = Regex::new(r"(?<=a|ab)x").unwrap();
assert!(re.is_match("ax"));
assert!(re.is_match("abx"));
assert!(!re.is_match("bx"));
}
#[test]
fn variable_length_negative_lookbehind() {
let re = Regex::new(r"(?<!ab|abc)x").unwrap();
assert!(re.is_match("cx"));
assert!(!re.is_match("abx"));
}
#[test]
fn subroutine_call_with_quantifier() {
let re = Regex::new(r"(?<r>a+)\g<r>").unwrap();
assert!(re.is_match("aaa"));
assert!(!re.is_match("b"));
}
#[test]
fn subroutine_call_in_if_else() {
let re = Regex::new(r"(?<a>x)?(?(<a>)\g<a>|y)").unwrap();
assert!(re.is_match("xx"));
assert!(re.is_match("y"));
}
#[test]
fn recursive_capture_group() {
let re = Regex::new(r"^(?<r>a(?:\g<r>)?b)$").unwrap();
assert!(re.is_match("ab"));
assert!(re.is_match("aabb"));
assert!(re.is_match("aaabbb"));
assert!(!re.is_match("aab"));
}
#[test]
fn backref_with_level_named() {
let re = Regex::new(r"(?<a>[a-z])(?:\g<a>\k<a+0>)?").unwrap();
assert!(re.is_match("a"));
assert!(re.is_match("abc"));
}
#[test]
fn word_ascii_star_optimization() {
let re = Regex::new(r"(?W)\w*$").unwrap();
assert!(re.is_match("hello"));
assert!(re.is_match(""));
}
#[test]
fn word_ascii_star_peek_next_optimization() {
let re = Regex::new(r"(?W)\w*x").unwrap();
assert!(re.is_match("abcx"));
assert!(re.is_match("x"));
assert!(!re.is_match("abc"));
}
#[test]
fn scanner_g_anchor_pattern() {
use ferroni::scanner::{Scanner, ScannerFindOptions};
let mut scanner = Scanner::new(&[r"\G\w+", r"\s+"]).unwrap();
let input = "hello world";
let result = scanner.find_next_match(input, 0, ScannerFindOptions::NONE);
assert!(result.is_some());
let m = result.unwrap();
assert_eq!(m.index, 0); assert_eq!(m.capture_indices[0].start, 0);
}
#[test]
fn char_class_dash_in_range_context() {
let re = Regex::new(r"[!--]").unwrap();
assert!(re.is_match("!"));
assert!(re.is_match("-"));
assert!(re.is_match("#"));
assert!(!re.is_match("0"));
}
#[test]
fn char_class_double_range() {
let re = Regex::new(r"[0-9-a]").unwrap();
assert!(re.is_match("5"));
assert!(re.is_match("a"));
assert!(re.is_match("-"));
}
#[test]
fn absent_expression_basic() {
let re = Regex::new(r"(?~abc)").unwrap();
assert!(re.is_match("xyz"));
}
#[test]
fn absent_expression_with_range() {
let re = Regex::new(r"\w+(?~abc)\w+").unwrap();
assert!(re.is_match("xyzdef"));
}
#[test]
fn conditional_define_pattern() {
let re = Regex::new(r"(?(DEFINE)(?<d>\d+))\g<d>-\g<d>").unwrap();
assert!(re.is_match("123-456"));
assert!(!re.is_match("abc-def"));
}
#[test]
fn named_capture_in_lookahead() {
let re = Regex::new(r"(?=(?<word>\w+))\w+").unwrap();
let caps = re.captures("hello").unwrap();
assert_eq!(caps.get(1).unwrap().as_str(), "hello");
}
#[test]
fn named_capture_in_lookbehind() {
let re = Regex::new(r"(?<=(?<prefix>ab))cd").unwrap();
assert!(re.is_match("abcd"));
}
#[test]
fn subroutine_with_alternation_avoids_infinite_recursion() {
let re = Regex::new(r"(?<r>a|b\g<r>c)").unwrap();
assert!(re.is_match("a"));
assert!(re.is_match("bac"));
assert!(re.is_match("bbaccc")); }
#[test]
fn many_literal_alternatives_trigger_trie() {
let re = Regex::new(r"abc|abd|abe|abf|abg|abh|abi|abj").unwrap();
assert!(re.is_match("abc"));
assert!(re.is_match("abj"));
assert!(!re.is_match("abz"));
}
#[test]
fn literal_alternatives_preserve_ordered_backtracking() {
let re = Regex::new(r"(?:foo|foobar|a1|a2|a3|a4|a5|a6)bar").unwrap();
let matched = re.find("foobar").expect("shorter first branch matches");
assert_eq!(matched.as_str(), "foobar");
let ordered = Regex::new(r"foo|foobar|a1|a2|a3|a4|a5|a6").unwrap();
assert_eq!(ordered.find("foobar").unwrap().as_str(), "foo");
}
#[test]
fn literal_alternatives_preserve_per_branch_case_options() {
let re = Regex::new(r"(?i)foo|bar|baz|(?-i:QUX)").unwrap();
assert!(!re.is_match("qux"));
assert!(re.is_match("QUX"));
}
#[test]
fn case_insensitive_literal_alternatives() {
let re = Regex::builder("ABC|DEF|GHI|JKL")
.case_insensitive(true)
.build()
.unwrap();
assert!(re.is_match("abc"));
assert!(re.is_match("jkl"));
}
#[test]
fn find_iter_bytes_method() {
let re = Regex::new_bytes(b"\\d+").unwrap();
let matches: Vec<_> = re.find_iter_bytes(b"12 34 56").collect();
assert_eq!(matches.len(), 3);
assert_eq!(matches[0].as_bytes(), b"12");
assert_eq!(matches[2].as_bytes(), b"56");
}
#[test]
fn builder_multi_line_anchors_default_matches_each_line() {
let re = Regex::builder(r"^b$").build().unwrap();
assert_eq!(re.find("a\nb\nc").unwrap().as_str(), "b");
}
#[test]
fn builder_multi_line_anchors_true_matches_each_line() {
let re = Regex::builder(r"^b$")
.multi_line_anchors(true)
.build()
.unwrap();
assert_eq!(re.find("a\nb\nc").unwrap().as_str(), "b");
}
#[test]
fn builder_multi_line_anchors_false_matches_only_input_boundaries() {
let re = Regex::builder(r"^b$")
.multi_line_anchors(false)
.build()
.unwrap();
assert!(re.find("a\nb\nc").is_none());
}
#[test]
fn builder_multi_line_anchors_toggles_without_affecting_other_options() {
let re = Regex::builder(" ^ a . b $ ")
.case_insensitive(true)
.dot_matches_newline(true)
.extended(true)
.multi_line_anchors(false)
.multi_line_anchors(true)
.multi_line_anchors(true)
.build()
.unwrap();
assert_eq!(re.find("before\nA\nB\nafter").unwrap().as_str(), "A\nB");
}
#[test]
fn builder_multi_line_anchors_overrides_syntax_defaults() {
use ferroni::regsyntax::{
OnigSyntaxJava, OnigSyntaxPerl, OnigSyntaxPosixBasic, OnigSyntaxPosixExtended,
};
for (name, syntax) in [
("Java", &OnigSyntaxJava),
("Perl", &OnigSyntaxPerl),
("POSIX basic", &OnigSyntaxPosixBasic),
("POSIX extended", &OnigSyntaxPosixExtended),
] {
let default = Regex::builder(r"^b$").syntax(syntax).build().unwrap();
assert!(default.find("a\nb\nc").is_none(), "{name} default");
let enabled = Regex::builder(r"^b$")
.syntax(syntax)
.multi_line_anchors(true)
.build()
.unwrap();
assert_eq!(
enabled.find("a\nb\nc").unwrap().as_str(),
"b",
"{name} enabled"
);
let disabled = Regex::builder(r"^b$")
.syntax(syntax)
.multi_line_anchors(false)
.build()
.unwrap();
assert!(disabled.find("a\nb\nc").is_none(), "{name} disabled");
}
}
#[test]
fn captures_len_and_is_empty() {
let re = Regex::new(r"(a)(b)(c)").unwrap();
let caps = re.captures("abc").unwrap();
assert_eq!(caps.len(), 4); assert!(!caps.is_empty());
}
#[test]
fn captures_named_group_lookup() {
let re = Regex::new(r"(?<year>\d{4})-(?<month>\d{2})").unwrap();
let caps = re.captures("2024-03").unwrap();
let year = caps.name("year");
assert!(year.is_some());
assert_eq!(year.unwrap().as_str(), "2024");
}
#[test]
fn null_byte_octal_escape() {
let re = Regex::new_bytes(b"\\0").unwrap();
assert!(re.is_match_bytes(&[0x00]));
assert!(!re.is_match_bytes(&[0x30])); }
#[test]
fn ruby_u_hex_escape() {
use ferroni::regsyntax::OnigSyntaxRuby;
let re = Regex::builder(r"\u0041")
.syntax(&OnigSyntaxRuby)
.build()
.unwrap();
assert!(re.is_match("A")); assert!(!re.is_match("B"));
}
#[test]
fn escaped_non_ascii_literal() {
let re = Regex::new(r"\ä").unwrap();
assert!(re.is_match("ä"));
}
#[test]
fn subroutine_call_in_lookbehind_context() {
let re = Regex::new(r"(?<d>ab)(?<=\g<d>)x");
if let Ok(re) = re {
let _ = re.is_match("abx");
}
}
#[test]
fn recursive_pattern_in_alternation() {
let re = Regex::new(r"^(?<r>a|b\g<r>c)$").unwrap();
assert!(re.is_match("a"));
assert!(re.is_match("bac"));
assert!(re.is_match("bbacc"));
}
#[test]
fn subroutine_in_quantifier_state() {
let re = Regex::new(r"(?<r>\w)\g<r>{2,5}").unwrap();
assert!(re.is_match("abcde"));
}
#[test]
fn subroutine_in_lookahead_state() {
let re = Regex::new(r"(?<d>\d)(?=\g<d>)\d").unwrap();
assert!(re.is_match("12"));
}
#[test]
fn subroutine_in_negative_lookbehind_state() {
let re = Regex::new(r"(?<d>[a-z]{2})(?<!\g<d>)\d");
if let Ok(re) = re {
let _ = re.is_match("ab1");
}
}
#[test]
fn scanner_begin_position_anchor_at_end_of_text_does_not_panic() {
use ferroni::scanner::{Scanner, ScannerFindOptions};
let mut scanner = Scanner::new(&[r"^\G"]).unwrap();
assert!(
scanner
.find_next_match("h", 0, ScannerFindOptions::NONE)
.is_some()
);
assert!(
scanner
.find_next_match("h", 1, ScannerFindOptions::NONE)
.is_none()
);
let mut scanner = Scanner::new(&[r"^\Ga"]).unwrap();
assert!(
scanner
.find_next_match("a", 1, ScannerFindOptions::NONE)
.is_none()
);
}
#[test]
fn search_over_truncated_multibyte_lead_stays_inside_haystack() {
use ferroni::encodings::utf8::ONIG_ENCODING_UTF8;
use ferroni::oniguruma::{ONIG_MISMATCH, ONIG_OPTION_NONE, OnigRegion};
use ferroni::regcomp::onig_new;
use ferroni::regexec::onig_search;
use ferroni::regsyntax::OnigSyntaxOniguruma;
let reg = onig_new(
b"$",
ONIG_OPTION_NONE,
&ONIG_ENCODING_UTF8,
&OnigSyntaxOniguruma,
)
.unwrap();
let haystack: &[u8] = &[0x00, 0xE6, 0xE6];
for region in [None, Some(OnigRegion::new())] {
let (position, _) = onig_search(
®,
haystack,
haystack.len(),
0,
haystack.len(),
region,
ONIG_OPTION_NONE,
);
assert_eq!(position, haystack.len() as i32);
}
let reg = onig_new(
b"x$",
ONIG_OPTION_NONE,
&ONIG_ENCODING_UTF8,
&OnigSyntaxOniguruma,
)
.unwrap();
let (position, _) = onig_search(
®,
haystack,
haystack.len(),
0,
haystack.len(),
None,
ONIG_OPTION_NONE,
);
assert_eq!(position, ONIG_MISMATCH);
}
#[test]
fn optimizer_retry_over_truncated_multibyte_lead_stays_inside_haystack() {
use ferroni::encodings::utf8::ONIG_ENCODING_UTF8;
use ferroni::oniguruma::{ONIG_MISMATCH, ONIG_OPTION_NONE};
use ferroni::regcomp::onig_new;
use ferroni::regexec::onig_search;
use ferroni::regsyntax::OnigSyntaxOniguruma;
let reg = onig_new(
br"^\s$",
ONIG_OPTION_NONE,
&ONIG_ENCODING_UTF8,
&OnigSyntaxOniguruma,
)
.unwrap();
let haystack: &[u8] = &[b'w', 0xC8];
let (position, _) = onig_search(
®,
haystack,
haystack.len(),
0,
haystack.len(),
None,
ONIG_OPTION_NONE,
);
assert_eq!(position, ONIG_MISMATCH);
let text = "w\u{3000}\u{3000}\n\u{3000}";
let (position, _) = onig_search(
®,
text.as_bytes(),
text.len(),
0,
2,
None,
ONIG_OPTION_NONE,
);
assert_eq!(position, ONIG_MISMATCH);
}
#[test]
fn stacked_possessive_quantifiers_compile_and_match() {
use ferroni::encodings::utf8::ONIG_ENCODING_UTF8;
use ferroni::oniguruma::{ONIG_OPTION_NONE, OnigRegion};
use ferroni::regcomp::onig_new;
use ferroni::regexec::onig_search;
use ferroni::regsyntax::OnigSyntaxPerl_NG;
let mut pattern = b"un".to_vec();
pattern.extend(std::iter::repeat_n(b'+', 43));
pattern.extend_from_slice(b"ct?");
let reg = onig_new(
&pattern,
ONIG_OPTION_NONE,
&ONIG_ENCODING_UTF8,
&OnigSyntaxPerl_NG,
)
.unwrap();
let haystack = b"xunnnct";
let (position, region) = onig_search(
®,
haystack,
haystack.len(),
0,
haystack.len(),
Some(OnigRegion::new()),
ONIG_OPTION_NONE,
);
assert_eq!(position, 1);
assert_eq!(region.unwrap().end[0], 7);
}
#[test]
fn left_recursive_subexpression_call_is_rejected_at_compile_time() {
use ferroni::scanner::Scanner;
assert!(Scanner::new(&["\\g<0>a|b|"]).is_err());
assert!(Regex::new(r"\g<0>|a").is_err());
let re = Regex::new(r"a\g<0>|b").unwrap();
assert_eq!(re.find("aab").unwrap().as_str(), "aab");
}
#[test]
fn scanner_reports_the_keep_adjusted_match() {
use ferroni::scanner::{Scanner, ScannerFindOptions};
let spans = |patterns: &[&str], text: &str, position: usize| {
Scanner::new(patterns)
.unwrap()
.find_next_match(text, position, ScannerFindOptions::NONE)
.map(|m| {
(
m.index,
m.capture_indices[0].start,
m.capture_indices[0].end,
)
})
};
assert_eq!(spans(&[r"a\Kb"], "xxabxx", 0), Some((0, 3, 4)));
assert_eq!(spans(&[r"ab\K"], "xxabxx", 0), Some((0, 4, 4)));
assert_eq!(spans(&[r"a\K(b)"], "xxabxx", 0), Some((0, 3, 4)));
assert_eq!(spans(&[r"\Kab"], "xxabxx", 0), Some((0, 2, 4)));
assert_eq!(spans(&[r"zz", r"a\Kb"], "xxabxx", 0), Some((1, 3, 4)));
let mut scanner = Scanner::new(&[r"a\Kb"]).unwrap();
for _ in 0..32 {
let m = scanner
.find_next_match_with_id("xxabxx", 1, 0, ScannerFindOptions::NONE)
.unwrap();
assert_eq!(
(m.capture_indices[0].start, m.capture_indices[0].end),
(3, 4)
);
}
}
#[test]
fn scanner_keep_match_stays_on_character_boundaries() {
use ferroni::scanner::{Scanner, ScannerFindOptions};
let text = "\u{FFFD}a\u{FFFD}";
let pattern = format!(".*\\K{}", '\u{FFFD}');
let mut scanner = Scanner::new(&[pattern.as_str()]).unwrap();
for position in (0..=text.len()).filter(|p| text.is_char_boundary(*p)) {
let Some(m) = scanner.find_next_match(text, position, ScannerFindOptions::NONE) else {
assert_eq!(position, text.len());
continue;
};
let whole = &m.capture_indices[0];
assert_eq!(
(whole.start, whole.end),
(4, 7),
"match from position {position}"
);
assert!(text.is_char_boundary(whole.start) && text.is_char_boundary(whole.end));
}
}