use ferroni::api::{Regex, RegexBuilder};
use ferroni::error::RegexError;
use ferroni::oniguruma::{
ONIGERR_INVALID_BACKREF, ONIGERR_UNDEFINED_GROUP_REFERENCE, OnigSyntaxType,
};
use ferroni::prelude::*;
use ferroni::regint::DEFAULT_PARSE_DEPTH_LIMIT;
use ferroni::regsyntax::{
OnigSyntaxOniguruma, OnigSyntaxPerl_NG, OnigSyntaxPython, OnigSyntaxRuby,
};
#[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);
}
fn error_message(syntax: &'static OnigSyntaxType, pattern: &str) -> String {
Regex::builder(pattern)
.syntax(syntax)
.build()
.unwrap_err()
.to_string()
}
#[test]
fn error_message_names_invalid_char_property() {
let cases = [
(r"\p{Nope}", "{Nope}"),
(r"\p{^Nope}", "{Nope}"),
(r"[\p{Nope}]", "{Nope}"),
(r"\pQ", "{Q}"),
(r"\p{}", "{}"),
(r"\p{Ñope}", "{Ñope}"),
(
r"\p{abcdefghijklmnopqrstuvwxyz0123456789}",
"{abcdefghijklmnopqrstuvwxyz0...}",
),
];
for (pattern, name) in cases {
assert_eq!(
Regex::new(pattern).unwrap_err().to_string(),
format!("syntax error: invalid character property name {name}"),
"{pattern}"
);
}
}
#[test]
fn error_message_names_undefined_name_reference() {
let cases = [
(&OnigSyntaxOniguruma, r"\k<nope>"),
(&OnigSyntaxOniguruma, r"\g<nope>"),
(&OnigSyntaxOniguruma, r"(?(<nope>)a|b)"),
(&OnigSyntaxPerl_NG, r"(?&nope)"),
(&OnigSyntaxPython, r"(?P=nope)"),
(&OnigSyntaxPython, r"(?P>nope)"),
];
for (syntax, pattern) in cases {
assert_eq!(
error_message(syntax, pattern),
"syntax error: undefined name <nope> reference",
"{pattern}"
);
}
}
#[test]
fn error_message_names_undefined_group_reference() {
let cases = [
(&OnigSyntaxOniguruma, r"\g<5>", "<5>"),
(&OnigSyntaxOniguruma, r"\g<+1>", "<+1>"),
(&OnigSyntaxOniguruma, r"\g<-2>", "<-2>"),
(&OnigSyntaxPerl_NG, r"(?1)", "<1>"),
(&OnigSyntaxPerl_NG, r"(?-2)", "<-2>"),
];
for (syntax, pattern, name) in cases {
assert_eq!(
error_message(syntax, pattern),
format!("syntax error: undefined group {name} reference"),
"{pattern}"
);
}
}
#[test]
fn error_message_names_multiplex_defined_name() {
assert_eq!(
error_message(&OnigSyntaxPython, r"(?P<a>x)(?P<a>y)"),
"syntax error: multiplex defined name <a>"
);
for pattern in [
r"(*COUNT[AB]{X})(*COUNT[AB]{X})",
r"(*COUNT[AB]{X})a(*MAX[AB]{2})",
] {
assert_eq!(
Regex::new(pattern).unwrap_err().to_string(),
"syntax error: multiplex defined name <AB>",
"{pattern}"
);
}
}
#[test]
fn error_message_names_multiplex_definition_name_call() {
for syntax in [&OnigSyntaxOniguruma, &OnigSyntaxRuby] {
assert_eq!(
error_message(syntax, r"(?<a>x)(?<a>y)\g<a>"),
"syntax error: multiplex definition name <a> call"
);
}
}
#[test]
fn error_message_names_invalid_group_name() {
let cases = [
(r"(?<1a>x)", "<1a>"),
(r"\k<1a>", "<1a>"),
(r"\k<+0>", "<+0>"),
(r"\k<+>", "<+>"),
(r"\k<-x>", "<-x>"),
(r"\k<a+1x>", "<a+1x>>"),
(r"\k<1+x>", "<1+x>>"),
];
for (pattern, name) in cases {
assert_eq!(
Regex::new(pattern).unwrap_err().to_string(),
format!("syntax error: invalid group name {name}"),
"{pattern}"
);
}
}
#[test]
fn error_message_names_invalid_char_in_group_name() {
let cases = [
(r"(?<$a>x)", "<$a>"),
(r"(?<$ab", "<$ab>"),
(r"\k<a$b>", "<a$b>"),
(r"\k<a$b", "<a$>"),
];
for (pattern, name) in cases {
assert_eq!(
Regex::new(pattern).unwrap_err().to_string(),
format!("syntax error: invalid char in group name {name}"),
"{pattern}"
);
}
}
fn assert_compiles_like_c(cases: &[(&'static OnigSyntaxType, &str, Option<&str>)]) {
for &(syntax, pattern, expected) in cases {
let got = Regex::builder(pattern)
.syntax(syntax)
.build()
.err()
.map(|e| e.to_string());
let expected = expected.map(|m| format!("syntax error: {m}"));
assert_eq!(got, expected, "{pattern}");
}
}
#[test]
fn group_names_parse_like_c() {
let onig = &OnigSyntaxOniguruma;
assert_compiles_like_c(&[
(onig, r"(?<a$b>x)", None),
(onig, r"(?'a-b'x)", None),
(&OnigSyntaxPython, r"(?P<a+1>x)", None),
(onig, r"(?<ab", Some("invalid group name <a>")),
(onig, r"(?<a$b)", Some("invalid group name <a$b>")),
(onig, r"\k<a", Some("invalid group name <a>")),
(onig, r"\k<a)", Some("invalid group name <a)>")),
(onig, r"\k<a+>", Some("invalid group name <a+>>")),
(onig, r"\k<1->", Some("invalid group name <1->>")),
(onig, r"\k<$a>", Some("invalid char in group name <$a>")),
(onig, r"\k<$a", Some("invalid char in group name <$>")),
(onig, r"\g<ab", Some("invalid group name <a>")),
(onig, r"\g<a)", Some("invalid group name <a>")),
(onig, r"\g<a-b", Some("invalid group name <a->")),
(onig, r"\g<1a>", Some("undefined name <1a> reference")),
(onig, r"\g<a+1>", Some("undefined name <a+1> reference")),
(onig, r"\g<a$b>", Some("undefined name <a$b> reference")),
(
&OnigSyntaxPerl_NG,
r"(?&a-1)",
Some("undefined name <a-1> reference"),
),
(
&OnigSyntaxPerl_NG,
r"(?+0)",
Some("invalid group name <+0>"),
),
(
onig,
r"(x)\k<+2147483647>",
Some("invalid backref number/name"),
),
]);
}
#[test]
fn python_named_backref_and_call_parse_like_c() {
let python = &OnigSyntaxPython;
assert_compiles_like_c(&[
(python, r"(?P=1)", Some("invalid backref number/name")),
(python, r"(?P=a-b)", Some("invalid group name <a-b)>")),
(python, r"(?P=1a)", Some("invalid group name <1a>")),
(python, r"(?P>1a)", Some("undefined name <1a> reference")),
(python, r"(?P>+0)", Some("invalid group name <+0>")),
(python, r"(?P<a>x)(?P=a)", None),
(python, r"(?P<a>x)(?P>a)", None),
]);
let err = Regex::builder(r"(?P>1)")
.syntax(python)
.build()
.unwrap_err();
assert_eq!(err.code(), ONIGERR_UNDEFINED_GROUP_REFERENCE);
}
#[test]
fn numbered_backrefs_are_checked_like_c() {
let onig = &OnigSyntaxOniguruma;
let not_allowed = Some("numbered backref/call is not allowed. (use name)");
assert_compiles_like_c(&[
(onig, r"(?<a>x)\k<2>", not_allowed),
(onig, r"(?<a>x)\k<+1>", not_allowed),
(onig, r"(?<a>x)(y)\k<3>", not_allowed),
(&OnigSyntaxPerl_NG, r"(?<a>x)\k<2>", not_allowed),
(onig, r"\k<2>", Some("invalid backref number/name")),
(onig, r"(x)\k<2>", Some("invalid backref number/name")),
(onig, r"\k<1>(x)", None),
]);
}
#[test]
fn numbered_calls_are_checked_like_c() {
let onig = &OnigSyntaxOniguruma;
let not_allowed = Some("numbered backref/call is not allowed. (use name)");
assert_compiles_like_c(&[
(onig, r"(?<a>x)\g<1>", not_allowed),
(onig, r"(?<a>x)\g<0>", not_allowed),
(onig, r"(?<a>x)\g<2>", not_allowed),
(onig, r"(?<a>x)\g<-1>", not_allowed),
(&OnigSyntaxRuby, r"(?<a>x)\g<1>", not_allowed),
(&OnigSyntaxPerl_NG, r"(?<a>x)(?1)", not_allowed),
(&OnigSyntaxPerl_NG, r"(?<a>x)\g<1>", not_allowed),
(onig, r"(x)\g<1>", None),
(onig, r"(?<a>x)\g<a>", None),
]);
}
#[test]
fn conditions_parse_like_c() {
let onig = &OnigSyntaxOniguruma;
let not_allowed = Some("numbered backref/call is not allowed. (use name)");
assert_compiles_like_c(&[
(onig, r"(a)(?(1)a|b)", None),
(onig, r"(a)(?(1+0)a|b)", None),
(onig, r"(a)(?(<1>)a|b)", None),
(onig, r"(a)(?(1))", None),
(onig, r"(?(1a)a|b)", None),
(onig, r"(?(-0)a|b)", None),
(onig, r"(?(a)b)", None),
(
onig,
r"(?(+0)a|b)",
Some("target of repeat operator is not specified"),
),
(onig, r"(?(1)a|b)", Some("invalid backref number/name")),
(
onig,
r"(?(<nope>)a|b)",
Some("undefined name <nope> reference"),
),
(onig, r"(?(<1>a|b)", Some("end pattern in group")),
(onig, r"(?(1", Some("end pattern in group")),
(onig, r"(?(a", Some("end pattern in group")),
(onig, r"(?(a))", Some("invalid if-else syntax")),
(onig, r"(?<a>x)(?(<2>)a|b)", not_allowed),
(onig, r"(?<a>x)(?(2)a|b)", not_allowed),
]);
}
#[test]
fn invalid_interval_brace_is_a_literal_like_c() {
let onig = &OnigSyntaxOniguruma;
assert_compiles_like_c(&[
(onig, r"{(", Some("end pattern with unmatched parenthesis")),
(onig, r"{)", Some("unmatched close parenthesis")),
(onig, r"{[", Some("premature end of char-class")),
(onig, r"a{(?", Some("end pattern in group")),
(
onig,
r"a{1,x}(",
Some("end pattern with unmatched parenthesis"),
),
(
&OnigSyntaxRuby,
r"{(",
Some("end pattern with unmatched parenthesis"),
),
(
&OnigSyntaxPython,
r"a{(",
Some("end pattern with unmatched parenthesis"),
),
(onig, r"a{1,x}", None),
(onig, r"a{,2}", None),
(onig, r"a{", None),
]);
let re = Regex::new(r"a{1,x}(b)").unwrap();
assert_eq!(re.find("xa{1,x}b").unwrap().as_str(), "a{1,x}b");
let re = Regex::new(r"a{2}").unwrap();
assert!(re.is_match("aa") && !re.is_match("a{2}"));
}
#[test]
fn non_ascii_after_qmark_is_not_an_option_like_c() {
let onig = &OnigSyntaxOniguruma;
let undefined = Some("undefined group option");
assert_compiles_like_c(&[
(onig, "(?\u{013A}a)", undefined), (onig, "(?\u{013D}a)", undefined), (onig, "(?\u{0121}a)", undefined), (onig, "(?\u{013E}a)", undefined), (onig, "(?\u{0169})a", undefined), (onig, "(?i\u{0169})a", undefined),
(onig, "(?i\u{0178})a", undefined), (onig, "(?y{\u{0167}})a", undefined), ]);
}
#[test]
fn escaped_non_ascii_in_char_class_is_literal_like_c() {
let re = Regex::new("[\\\u{0177}]").unwrap();
assert!(re.is_match("\u{0177}"));
assert!(!re.is_match("a"));
let re = Regex::new("[\\\u{0164}]").unwrap();
assert!(re.is_match("\u{0164}"));
assert!(!re.is_match("1"));
}
#[test]
fn callout_numbers_by_tag_match_c() {
use ferroni::encodings::utf8::ONIG_ENCODING_UTF8;
use ferroni::oniguruma::{ONIG_OPTION_NONE, ONIGERR_INVALID_CALLOUT_TAG_NAME};
use ferroni::regcomp::onig_new;
use ferroni::regexec::onig_get_callout_num_by_tag;
for (pattern, tag, num) in [
(r"(*FAIL[t])|x", "t", 1),
(r"(*FAIL)(*COUNT[t]{X})|x", "t", 2),
(r"(*COUNT[a]{X})(*FAIL[t])|x", "t", 2),
(r"a(*FAIL[t])|(*MAX[u]{2})a", "u", 2),
(r"(*FAIL)|a", "t", ONIGERR_INVALID_CALLOUT_TAG_NAME),
(r"(*FAIL[t])|x", "u", ONIGERR_INVALID_CALLOUT_TAG_NAME),
] {
let reg = onig_new(
pattern.as_bytes(),
ONIG_OPTION_NONE,
&ONIG_ENCODING_UTF8,
&OnigSyntaxOniguruma,
)
.unwrap();
assert_eq!(
onig_get_callout_num_by_tag(®, tag.as_bytes()),
num,
"{pattern}"
);
}
}
#[test]
fn callout_arguments_are_scanned_before_the_name_like_c() {
let onig = &OnigSyntaxOniguruma;
let bad = Some("invalid callout pattern");
assert_compiles_like_c(&[
(onig, r"(*kx{|&", bad),
(onig, r"(*NOPE{1", bad),
(onig, r"(*MAX{2", bad),
(onig, r"(*NOPE{1})", Some("undefined callout name")),
(onig, r"(*MAX{2})", None),
]);
}
#[test]
fn condition_bodies_must_be_closed_like_c() {
let onig = &OnigSyntaxOniguruma;
let unclosed = Some("end pattern with unmatched parenthesis");
assert_compiles_like_c(&[
(onig, r"(?()1", unclosed),
(onig, r"(?(a)b", unclosed),
(onig, r"(a)(?(1)b", unclosed),
(onig, r"(a)(?(1)b|c", unclosed),
(onig, r"(?(a)(?:b|c))", None),
(onig, r"(a)(?(1)b|c|d)", None),
]);
}
#[test]
fn callout_names_and_tags_parse_like_c() {
let onig = &OnigSyntaxOniguruma;
let bad_tag = Some("invalid callout tag name");
assert_compiles_like_c(&[
(onig, r"(*FAIL[a])", None),
(onig, r"(*COUNT[_a]{X})", None),
(onig, r"(?{foo}[a])", None),
(onig, r"a|(*FAIL)", None),
(onig, r"(*FAIL[1])", bad_tag),
(onig, r"(*FAIL[])", bad_tag),
(onig, r"(*FAIL[a$b])", bad_tag),
(onig, r"(*MAX[1]{2})", bad_tag),
(onig, r"(?{foo}[1])", bad_tag),
(
onig,
r"(*FAIL[a])(*FAIL[a])",
Some("multiplex defined name <a>"),
),
(
onig,
r"(?{foo}[a])(?{bar}[a])",
Some("multiplex defined name <a>"),
),
(onig, r"(*FA-IL)", Some("invalid callout name")),
(onig, r"(*NOPE)", Some("undefined callout name")),
(onig, r"(*FAIL{1})", Some("invalid callout arg")),
(onig, r"(*FAIL[a]", Some("end pattern in group")),
]);
}
#[test]
fn group_options_start_like_c() {
let onig = &OnigSyntaxOniguruma;
let undefined = Some("undefined group option");
assert_compiles_like_c(&[
(onig, r"(?)", undefined),
(onig, r"(?Q)", undefined),
(&OnigSyntaxRuby, r"(?)", undefined),
(&OnigSyntaxPerl_NG, r"(?)", undefined),
(&OnigSyntaxRuby, r"(?W)a", undefined),
(onig, r"(?i)a", None),
(onig, r"(?-i)a", None),
(onig, r"(?W)a", None),
(onig, r"(?C)a", None),
(onig, r"(?I)a", None),
]);
}
#[test]
fn pattern_ending_after_lt_is_reported_like_c() {
let unclosed = Some("end pattern with unmatched parenthesis");
assert_compiles_like_c(&[
(&OnigSyntaxOniguruma, r"(?<", unclosed),
(&OnigSyntaxOniguruma, r"(x)(?<", unclosed),
(&OnigSyntaxOniguruma, r"(?<=", unclosed),
(&OnigSyntaxPerl_NG, r"(?<", unclosed),
]);
}
#[test]
fn error_message_without_recorded_name_drops_placeholder() {
assert_compiles_like_c(&[
(
&OnigSyntaxPython,
r"(?P>1)",
Some("undefined group reference"),
),
(&OnigSyntaxPerl_NG, r"(?1a)", Some("invalid group name")),
(&OnigSyntaxPerl_NG, r"(?1+x)", Some("invalid group name")),
(
&OnigSyntaxOniguruma,
r"\p{}",
Some("invalid character property name {}"),
),
]);
}
#[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"(?(9999999999999)a|b)".as_slice(),
br"(a)(?(1+9999999999999)a|b)".as_slice(),
] {
assert!(Regex::new_bytes(pattern).is_ok(), "{pattern:?}");
}
for pattern in [
br"(a)\k<a+999999999999999>".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 begin_line_does_not_match_at_end_after_trailing_newline() {
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_match, onig_search};
use ferroni::regsyntax::OnigSyntaxOniguruma;
let compile = |pattern: &str| {
onig_new(
pattern.as_bytes(),
ONIG_OPTION_NONE,
&ONIG_ENCODING_UTF8,
&OnigSyntaxOniguruma,
)
.unwrap()
};
for (pattern, text, start, range, expected) in [
("^", "a\n", 2, 2, ONIG_MISMATCH),
("^", "a\n", 1, 2, ONIG_MISMATCH),
("^", "\n", 1, 1, ONIG_MISMATCH),
("^$", "a\n", 1, 2, ONIG_MISMATCH),
("(?m)^.*", "a\n", 2, 2, ONIG_MISMATCH),
("^", "a\nb\n", 3, 4, ONIG_MISMATCH),
("^", "a\n", 2, 0, 0),
("^", "a\n\n", 1, 3, 2),
("^$", "a\n\n", 1, 3, 2),
("$", "a\n", 2, 2, 2),
] {
let reg = compile(pattern);
let bytes = text.as_bytes();
let (position, _) = onig_search(
®,
bytes,
bytes.len(),
start,
range,
Some(OnigRegion::new()),
ONIG_OPTION_NONE,
);
assert_eq!(
position, expected,
"{pattern:?} on {text:?} start={start} range={range}"
);
}
let reg = compile("^");
let (length, _) = onig_match(®, b"a\n", 2, 2, None, ONIG_OPTION_NONE);
assert_eq!(length, ONIG_MISMATCH);
let (length, _) = onig_match(®, b"\n\n", 2, 1, None, ONIG_OPTION_NONE);
assert_eq!(length, 0);
}
#[test]
fn find_iter_begin_line_skips_end_after_trailing_newline() {
let starts = |pattern: &str, text: &str| -> Vec<usize> {
Regex::new(pattern)
.unwrap()
.find_iter(text)
.map(|m| m.start())
.collect()
};
assert_eq!(starts(r"(?m)^$", "a\nb\n"), Vec::<usize>::new());
assert_eq!(starts(r"^", "a\nb\n"), vec![0, 2]);
assert_eq!(starts(r"^$", "a\n\n"), vec![2]);
assert_eq!(starts(r"$", "a\nb\n"), vec![1, 3, 4]);
}
#[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+1>").unwrap();
assert!(re.is_match("xxx"));
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+1>").unwrap();
assert!(re.is_match("xxx"));
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 unicode_17_script_properties_and_case_folding() {
for (property, character) in [
(r"\p{Sidetic}", '\u{10940}'),
(r"\p{Tolong_Siki}", '\u{11DB0}'),
(r"\p{Beria_Erfe}", '\u{16EA0}'),
(r"\p{Tai_Yo}", '\u{1E6C0}'),
] {
let re = Regex::new(property).unwrap();
assert!(re.is_match(&character.to_string()), "{property}");
}
let uppercase = Regex::new(&format!("(?i){}", '\u{16EA0}')).unwrap();
assert!(uppercase.is_match("\u{16ebb}"));
}
#[test]
fn unicode_incb_covers_every_section() {
let re = Regex::new(r"\p{InCB}").unwrap();
for (section, character) in [
("Linker", '\u{094D}'),
("Consonant", '\u{0915}'),
("Extend", '\u{0300}'),
("Extend", '\u{200D}'),
("Extend", '\u{0DCA}'),
] {
assert!(
re.is_match(&character.to_string()),
"{section} U+{:04X}",
character as u32
);
}
assert!(!re.is_match("a"));
}
#[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));
}
}
#[test]
fn inverted_capture_is_not_participating() {
let re = Regex::new(r"((?=(a|ab))a?){2}").unwrap();
let caps = re.captures("a").unwrap();
assert_eq!(caps.get(0).unwrap().range(), 0..0);
assert!(caps.get(1).is_none());
let group2 = caps.get(2).unwrap();
assert_eq!(group2.range(), 1..1);
assert_eq!(group2.as_str(), "");
let listed: Vec<_> = caps.iter().map(|m| m.map(|m| m.range())).collect();
assert_eq!(listed, vec![Some(0..0), None, Some(1..1)]);
let _ = format!("{caps:?}");
let named = Regex::new(r"(?<g>(?=(?<h>a|ab))a?){2}").unwrap();
let caps = named.captures("a").unwrap();
assert!(caps.name("g").is_none());
assert_eq!(caps.name("h").unwrap().range(), 1..1);
}