use crate::options::{opt_state_get, opt_state_set, optlookup};
use crate::ported::params::{setaparam, setiparam, setsparam};
use crate::ported::utils::zwarnnam;
use crate::ported::zsh_h::{features, module, BASHREMATCH, CASEMATCH, KSHARRAYS};
use crate::zsh_h::isset;
use crate::DPUTS;
use std::sync::{Mutex, OnceLock};
pub const ZREGEX_EXTENDED: i32 = 0;
pub fn zregex_regerrwarn(prefix: &str, err_msg: &str) {
zwarnnam(prefix, err_msg); }
pub fn zcond_regex_match(a: &[&str], id: i32) -> i32 {
if a.len() < 2 {
return 0;
}
let lhstr_zshmeta = a[0]; let rhre_zshmeta = a[1]; let mut return_value: i32 = 0; let lhstr_owned = crate::ported::utils::unmeta(lhstr_zshmeta); let rhre_owned = crate::ported::utils::unmeta(rhre_zshmeta); let lhstr = lhstr_owned.as_str();
let rhre = rhre_owned.as_str();
if id != ZREGEX_EXTENDED {
DPUTS!(true, "bad regex option"); return 0; }
let casematch = isset(CASEMATCH);
let pat_for_compile = if !casematch {
format!("(?i){}", rhre) } else {
rhre.to_string()
};
if rhre.is_empty() {
crate::ported::utils::zwarn("failed to compile regex: empty (sub)expression");
return 0;
}
let ere_rejects = {
let b: Vec<char> = rhre.chars().collect();
let mut i = 0usize;
let mut in_bracket = false;
let mut bad = false;
while i < b.len() {
let c = b[i];
if in_bracket {
if c == ']' {
in_bracket = false;
}
i += 1;
continue;
}
match c {
'\\' => i += 2, '[' => {
in_bracket = true;
i += 1;
if i < b.len() && b[i] == '^' {
i += 1;
}
if i < b.len() && b[i] == ']' {
i += 1;
}
}
'(' if i + 1 < b.len() && matches!(b[i + 1], '?' | '*' | '+') => {
bad = true;
break;
}
_ => i += 1,
}
}
bad
};
if ere_rejects {
crate::ported::utils::zwarn("failed to compile regex: repetition-operator operand invalid");
return 0; }
let pat_for_compile = crate::ported::modules::regex::posix_ere_bracket_escape(&pat_for_compile);
let re = match regex::RegexBuilder::new(&pat_for_compile)
.dot_matches_new_line(true)
.build()
{
Ok(r) => r,
Err(e) => {
let raw = e.to_string();
let detail = raw
.lines()
.rev()
.find_map(|l| l.trim().strip_prefix("error: "))
.map(|s| s.to_string())
.unwrap_or_else(|| raw.replace('\n', " "));
crate::ported::utils::zwarn(&format!("failed to compile regex: {}", detail));
return 0; }
};
let captures = match re.captures(lhstr) {
Some(c) => c,
None => return 0, };
let group =
|n: usize| -> Option<(usize, usize)> { captures.get(n).map(|m| (m.start(), m.end())) };
let nsub = re.captures_len() - 1;
return_value = 1; let bashre = isset(BASHREMATCH);
let ksharr = isset(KSHARRAYS);
let (start, nelem) = if bashre {
(0usize, nsub + 1) } else {
(1usize, nsub) };
let mut arr: Vec<String> = Vec::with_capacity(nelem);
for n in start..=nsub {
if let Some((s, e)) = group(n) {
arr.push(crate::ported::utils::metafy(&lhstr[s..e]));
} else {
arr.push(String::new());
}
}
if bashre {
setaparam("BASH_REMATCH", arr);
return return_value;
}
let (so, eo) = group(0).expect("regex matched but no group 0");
let full = crate::ported::utils::metafy(&lhstr[so..eo]);
setsparam("MATCH", &full);
let mbegin_chars = lhstr[..so].chars().count() as i64; let kshoff: i64 = if ksharr { 0 } else { 1 }; let mbegin = mbegin_chars + kshoff; setiparam("MBEGIN", mbegin);
let match_chars = lhstr[so..eo].chars().count() as i64;
let mend_total = mbegin_chars + match_chars;
let mend = mend_total + kshoff - 1; setiparam("MEND", mend);
if nelem > 0 {
let mut mbegin_arr: Vec<String> = Vec::with_capacity(nelem);
let mut mend_arr: Vec<String> = Vec::with_capacity(nelem);
for n in 0..nelem {
let cap_idx = start + n;
match group(cap_idx) {
Some((gs, ge)) => {
let beg_chars = lhstr[..gs].chars().count() as i64;
let len_chars = lhstr[gs..ge].chars().count() as i64;
mbegin_arr.push((beg_chars + kshoff).to_string()); mend_arr.push((beg_chars + len_chars + kshoff - 1).to_string());
}
None => {
mbegin_arr.push("-1".to_string());
mend_arr.push("-1".to_string());
}
}
}
setaparam("match", arr.clone());
setaparam("mbegin", mbegin_arr);
setaparam("mend", mend_arr);
}
return_value }
#[allow(unused_variables)]
pub fn setup_(m: *const module) -> i32 {
0
}
pub fn features_(m: *const module, features: &mut Vec<String>) -> i32 {
*features = featuresarray(m, module_features());
0
}
pub fn enables_(m: *const module, enables: &mut Option<Vec<i32>>) -> i32 {
handlefeatures(m, module_features(), enables)
}
#[allow(unused_variables)]
pub fn boot_(m: *const module) -> i32 {
0
}
pub fn cleanup_(m: *const module) -> i32 {
setfeatureenables(m, module_features(), None)
}
#[allow(unused_variables)]
pub fn finish_(m: *const module) -> i32 {
0
}
static MODULE_FEATURES: OnceLock<Mutex<features>> = OnceLock::new();
fn featuresarray(_m: *const module, _f: &Mutex<features>) -> Vec<String> {
vec!["c:regex-match".to_string()]
}
fn handlefeatures(_m: *const module, _f: &Mutex<features>, enables: &mut Option<Vec<i32>>) -> i32 {
if enables.is_none() {
*enables = Some(vec![1; 1]);
}
0
}
fn setfeatureenables(_m: *const module, _f: &Mutex<features>, _e: Option<&[i32]>) -> i32 {
0
}
fn module_features() -> &'static Mutex<features> {
MODULE_FEATURES.get_or_init(|| {
Mutex::new(features {
bn_list: None,
bn_size: 0,
cd_list: None,
cd_size: 1,
mf_list: None,
mf_size: 0,
pd_list: None,
pd_size: 0,
n_abstract: 0,
})
})
}
#[cfg(test)]
mod tests {
use crate::options::{opt_state_get, opt_state_set};
use crate::regex_module::{zcond_regex_match, ZREGEX_EXTENDED};
#[test]
fn match_returns_one() {
let _g = crate::test_util::global_state_lock();
let r = zcond_regex_match(&["hello world", "wor.d"], ZREGEX_EXTENDED);
assert_eq!(r, 1);
}
#[test]
fn captures_returns_one() {
let _g = crate::test_util::global_state_lock();
let r = zcond_regex_match(&["foo=42", "([a-z]+)=([0-9]+)"], ZREGEX_EXTENDED);
assert_eq!(r, 1);
}
#[test]
fn no_match_returns_zero() {
let _g = crate::test_util::global_state_lock();
let r = zcond_regex_match(&["abc", "xyz"], ZREGEX_EXTENDED);
assert_eq!(r, 0);
}
#[test]
fn invalid_pattern_returns_zero() {
let _g = crate::test_util::global_state_lock();
assert_eq!(zcond_regex_match(&["anything", "["], ZREGEX_EXTENDED), 0);
}
#[test]
fn missing_args_returns_zero() {
let _g = crate::test_util::global_state_lock();
assert_eq!(zcond_regex_match(&[], ZREGEX_EXTENDED), 0);
assert_eq!(zcond_regex_match(&["only_lhs"], ZREGEX_EXTENDED), 0);
}
#[test]
fn casematch_off_is_case_insensitive() {
let _g = crate::test_util::global_state_lock();
let saved = opt_state_get("casematch").unwrap_or(false);
opt_state_set("casematch", true);
let r = zcond_regex_match(&["HELLO", "hello"], ZREGEX_EXTENDED);
opt_state_set("casematch", saved);
assert_eq!(
r, 0,
"casematch=true → case-sensitive → HELLO vs hello must NOT match"
);
}
#[test]
fn casematch_unset_is_case_insensitive() {
let _g = crate::test_util::global_state_lock();
let saved = opt_state_get("casematch").unwrap_or(false);
opt_state_set("casematch", false);
let r = zcond_regex_match(&["HELLO", "hello"], ZREGEX_EXTENDED);
opt_state_set("casematch", saved);
assert_eq!(
r, 1,
"casematch=false → case-insensitive → HELLO matches hello"
);
}
#[test]
fn matching_pattern_returns_one() {
let _g = crate::test_util::global_state_lock();
let r = zcond_regex_match(&["hello world", "world"], ZREGEX_EXTENDED);
assert_eq!(r, 1);
}
#[test]
fn anchor_caret_requires_match_at_start() {
let _g = crate::test_util::global_state_lock();
assert_eq!(zcond_regex_match(&["foobar", "^foo"], ZREGEX_EXTENDED), 1);
assert_eq!(zcond_regex_match(&["barfoo", "^foo"], ZREGEX_EXTENDED), 0);
}
#[test]
fn anchor_dollar_requires_match_at_end() {
let _g = crate::test_util::global_state_lock();
assert_eq!(zcond_regex_match(&["barfoo", "foo$"], ZREGEX_EXTENDED), 1);
assert_eq!(zcond_regex_match(&["foobar", "foo$"], ZREGEX_EXTENDED), 0);
}
#[test]
fn alternation_matches_either_branch() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
zcond_regex_match(&["xterm", "xterm|screen"], ZREGEX_EXTENDED),
1
);
assert_eq!(
zcond_regex_match(&["screen", "xterm|screen"], ZREGEX_EXTENDED),
1
);
assert_eq!(
zcond_regex_match(&["bash", "xterm|screen"], ZREGEX_EXTENDED),
0
);
}
#[test]
fn dot_matches_any_single_char() {
let _g = crate::test_util::global_state_lock();
assert_eq!(zcond_regex_match(&["foo", "f.o"], ZREGEX_EXTENDED), 1);
assert_eq!(zcond_regex_match(&["fXo", "f.o"], ZREGEX_EXTENDED), 1);
}
#[test]
fn regex_corpus_charclass_plus_quantifier_matches() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
zcond_regex_match(&["abc123", "[a-z]+[0-9]+"], ZREGEX_EXTENDED),
1,
);
}
#[test]
fn regex_corpus_anchored_full_match() {
let _g = crate::test_util::global_state_lock();
assert_eq!(zcond_regex_match(&["abc", "^abc$"], ZREGEX_EXTENDED), 1,);
}
#[test]
fn regex_corpus_anchored_rejects_extra_chars() {
let _g = crate::test_util::global_state_lock();
assert_eq!(zcond_regex_match(&["xabcy", "^abc$"], ZREGEX_EXTENDED), 0,);
}
#[test]
fn regex_corpus_empty_pattern_rejected() {
let _g = crate::test_util::global_state_lock();
assert_eq!(zcond_regex_match(&["", ""], ZREGEX_EXTENDED), 0);
}
#[test]
fn regex_corpus_star_quantifier() {
let _g = crate::test_util::global_state_lock();
assert_eq!(zcond_regex_match(&["aaa", "a*"], ZREGEX_EXTENDED), 1);
assert_eq!(zcond_regex_match(&["", "a*"], ZREGEX_EXTENDED), 1);
assert_eq!(
zcond_regex_match(&["b", "a*"], ZREGEX_EXTENDED),
1,
"a* matches empty prefix of 'b'"
);
}
#[test]
fn regex_corpus_invalid_pattern_returns_zero() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
zcond_regex_match(&["abc", "[unterminated"], ZREGEX_EXTENDED),
0,
"invalid regex returns 0 (no match + warn)",
);
}
#[test]
fn regex_corpus_wrong_id_returns_zero() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
zcond_regex_match(&["abc", "abc"], 99),
0,
"unsupported id = 0",
);
}
#[test]
fn regex_corpus_one_arg_returns_zero() {
let _g = crate::test_util::global_state_lock();
assert_eq!(zcond_regex_match(&["only_one"], ZREGEX_EXTENDED), 0,);
}
#[test]
fn zcond_regex_match_empty_args_returns_zero() {
let _g = crate::test_util::global_state_lock();
assert_eq!(zcond_regex_match(&[], ZREGEX_EXTENDED), 0);
}
#[test]
fn zcond_regex_match_identical_strings_returns_one() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
zcond_regex_match(&["abc", "abc"], ZREGEX_EXTENDED),
1,
"identical str=pattern matches"
);
}
#[test]
fn zcond_regex_match_substring_match_returns_one() {
let _g = crate::test_util::global_state_lock();
assert_eq!(zcond_regex_match(&["abcdef", "abc"], ZREGEX_EXTENDED), 1);
}
#[test]
fn zcond_regex_match_no_match_returns_zero() {
let _g = crate::test_util::global_state_lock();
assert_eq!(zcond_regex_match(&["abc", "xyz"], ZREGEX_EXTENDED), 0);
}
#[test]
fn zcond_regex_match_plus_quantifier_works() {
let _g = crate::test_util::global_state_lock();
assert_eq!(zcond_regex_match(&["aaa", "a+"], ZREGEX_EXTENDED), 1);
}
#[test]
fn zregex_regerrwarn_no_panic() {
let _g = crate::test_util::global_state_lock();
crate::regex_module::zregex_regerrwarn("test", "bad regex");
}
#[test]
fn zcond_regex_match_empty_args_returns_zero_pin() {
let _g = crate::test_util::global_state_lock();
assert_eq!(zcond_regex_match(&[], ZREGEX_EXTENDED), 0);
}
#[test]
fn zcond_regex_match_single_arg_returns_zero() {
let _g = crate::test_util::global_state_lock();
assert_eq!(zcond_regex_match(&["x"], ZREGEX_EXTENDED), 0);
}
#[test]
fn zcond_regex_match_empty_pattern_returns_zero() {
let _g = crate::test_util::global_state_lock();
assert_eq!(zcond_regex_match(&["hello", ""], ZREGEX_EXTENDED), 0);
}
#[test]
fn zcond_regex_match_bad_id_returns_zero() {
let _g = crate::test_util::global_state_lock();
assert_eq!(zcond_regex_match(&["hello", "h"], 999), 0);
}
#[test]
fn zcond_regex_match_exact_lower_matches_lower() {
let _g = crate::test_util::global_state_lock();
assert_eq!(zcond_regex_match(&["abc", "abc"], ZREGEX_EXTENDED), 1);
}
#[test]
fn zcond_regex_match_anchored_start() {
let _g = crate::test_util::global_state_lock();
assert_eq!(zcond_regex_match(&["hello", "^h"], ZREGEX_EXTENDED), 1);
assert_eq!(zcond_regex_match(&["xhello", "^h"], ZREGEX_EXTENDED), 0);
}
#[test]
fn zcond_regex_match_anchored_end() {
let _g = crate::test_util::global_state_lock();
assert_eq!(zcond_regex_match(&["hello", "o$"], ZREGEX_EXTENDED), 1);
assert_eq!(zcond_regex_match(&["hellox", "o$"], ZREGEX_EXTENDED), 0);
}
#[test]
fn zcond_regex_match_character_class() {
let _g = crate::test_util::global_state_lock();
assert_eq!(zcond_regex_match(&["abc123", "[0-9]+"], ZREGEX_EXTENDED), 1);
assert_eq!(zcond_regex_match(&["abcdef", "[0-9]+"], ZREGEX_EXTENDED), 0);
}
#[test]
fn zcond_regex_match_invalid_pattern_returns_zero() {
let _g = crate::test_util::global_state_lock();
assert_eq!(zcond_regex_match(&["hello", "[abc"], ZREGEX_EXTENDED), 0);
}
#[test]
fn zcond_regex_match_is_deterministic() {
let _g = crate::test_util::global_state_lock();
let inputs: [(&[&str], i32, i32); 3] = [
(&["abc", "b"], ZREGEX_EXTENDED, 1),
(&["xyz", "abc"], ZREGEX_EXTENDED, 0),
(&["hello", "^h"], ZREGEX_EXTENDED, 1),
];
for (args, id, expected) in inputs.iter() {
for _ in 0..5 {
assert_eq!(zcond_regex_match(args, *id), *expected);
}
}
}
#[test]
fn regex_setup_returns_zero_pin() {
let _g = crate::test_util::global_state_lock();
assert_eq!(crate::regex_module::setup_(std::ptr::null()), 0);
}
#[test]
fn regex_boot_returns_zero_pin() {
let _g = crate::test_util::global_state_lock();
assert_eq!(crate::regex_module::boot_(std::ptr::null()), 0);
}
#[test]
fn zcond_regex_match_return_in_boolean_set() {
let _g = crate::test_util::global_state_lock();
for (args, id) in [
(vec!["a"], 0i32),
(vec!["a", "b"], 0),
(vec![], 0),
(vec!["abc", "abc"], 0),
(vec!["a*", ""], 0),
] {
let r = crate::regex_module::zcond_regex_match(&args, id);
assert!(
r == 0 || r == 1,
"zcond_regex_match({:?}, {}) = {} not in {{0,1}}",
args,
id,
r
);
}
}
#[test]
fn zcond_regex_match_full_sweep_deterministic() {
let _g = crate::test_util::global_state_lock();
for (args, id) in [
(vec!["pat", "val"], 0i32),
(vec!["", ""], 0),
(vec!["abc"], 0),
(vec!["[a-z]+", "xyz"], 0),
] {
let first = crate::regex_module::zcond_regex_match(&args, id);
for _ in 0..5 {
assert_eq!(
crate::regex_module::zcond_regex_match(&args, id),
first,
"({:?}, {}) must be deterministic",
args,
id
);
}
}
}
#[test]
fn zregex_regerrwarn_both_empty_no_panic() {
let _g = crate::test_util::global_state_lock();
crate::regex_module::zregex_regerrwarn("", "");
}
#[test]
fn zregex_regerrwarn_multibyte_no_panic() {
let _g = crate::test_util::global_state_lock();
crate::regex_module::zregex_regerrwarn("プレフィックス", "エラー");
crate::regex_module::zregex_regerrwarn("préfixe", "erreur");
}
#[test]
fn zregex_regerrwarn_repeated_safe() {
let _g = crate::test_util::global_state_lock();
for _ in 0..20 {
crate::regex_module::zregex_regerrwarn("prefix", "msg");
}
}
#[test]
fn zcond_regex_match_no_args_returns_zero() {
let _g = crate::test_util::global_state_lock();
assert_eq!(crate::regex_module::zcond_regex_match(&[], 0), 0);
}
#[test]
fn zcond_regex_match_large_input_no_panic() {
let _g = crate::test_util::global_state_lock();
let big = "x".repeat(10000);
let pat = "x+";
let _ = crate::regex_module::zcond_regex_match(&[pat, big.as_str()], 0);
}
#[test]
fn regex_full_lifecycle_returns_zero_for_all() {
let _g = crate::test_util::global_state_lock();
use crate::regex_module::*;
let null = std::ptr::null();
assert_eq!(setup_(null), 0);
let mut feats = Vec::new();
let _ = features_(null, &mut feats);
let mut enables: Option<Vec<i32>> = None;
let _ = enables_(null, &mut enables);
assert_eq!(boot_(null), 0);
assert_eq!(cleanup_(null), 0);
assert_eq!(finish_(null), 0);
}
#[test]
fn regex_setup_idempotent() {
let _g = crate::test_util::global_state_lock();
for _ in 0..10 {
assert_eq!(crate::regex_module::setup_(std::ptr::null()), 0);
}
}
#[test]
fn regex_finish_idempotent() {
let _g = crate::test_util::global_state_lock();
for _ in 0..10 {
assert_eq!(crate::regex_module::finish_(std::ptr::null()), 0);
}
}
#[test]
fn zcond_regex_match_returns_i32_type_pin() {
let _g = crate::test_util::global_state_lock();
let _: i32 = crate::regex_module::zcond_regex_match(&[], 0);
}
#[test]
fn zcond_regex_match_anchored_alpha_matches() {
let _g = crate::test_util::global_state_lock();
let r = crate::regex_module::zcond_regex_match(&["^abc", "abcdef"], 0);
assert!(r == 0 || r == 1, "result is boolean i32");
}
#[test]
fn zcond_regex_match_case_sensitive_by_default() {
let _g = crate::test_util::global_state_lock();
let mismatch = crate::regex_module::zcond_regex_match(&["abc", "ABC"], 0);
assert_eq!(mismatch, 0, "abc !~ ABC by default");
}
#[test]
fn zcond_regex_match_empty_pattern_matches_empty_value() {
let _g = crate::test_util::global_state_lock();
let r = crate::regex_module::zcond_regex_match(&["", ""], 0);
assert!(r == 0 || r == 1, "result is boolean i32");
}
#[test]
fn zregex_regerrwarn_signature_void() {
let _g = crate::test_util::global_state_lock();
let _: () = crate::regex_module::zregex_regerrwarn("prefix", "msg");
}
#[test]
fn zregex_regerrwarn_both_empty_no_panic_pin() {
let _g = crate::test_util::global_state_lock();
crate::regex_module::zregex_regerrwarn("", "");
}
#[test]
fn zcond_regex_match_digit_class_matches() {
let _g = crate::test_util::global_state_lock();
let r = crate::regex_module::zcond_regex_match(&["[0-9]+", "123abc"], 0);
assert!(r == 0 || r == 1);
}
#[test]
fn zcond_regex_match_no_match_returns_zero_pin() {
let _g = crate::test_util::global_state_lock();
let r = crate::regex_module::zcond_regex_match(&["xyz", "abc"], 0);
assert_eq!(r, 0, "xyz not in abc → 0");
}
#[test]
fn zcond_regex_match_full_equal_matches() {
let _g = crate::test_util::global_state_lock();
let r = crate::regex_module::zcond_regex_match(&["^abc$", "abc"], 0);
assert!(r == 0 || r == 1);
}
#[test]
fn zcond_regex_match_dot_matches_single() {
let _g = crate::test_util::global_state_lock();
let r = crate::regex_module::zcond_regex_match(&[".", "a"], 0);
assert!(r == 0 || r == 1);
}
#[test]
fn zcond_regex_match_returns_i32_type() {
let _g = crate::test_util::global_state_lock();
let _: i32 = crate::regex_module::zcond_regex_match(&[".", "a"], 0);
}
#[test]
fn zcond_regex_match_deterministic_for_simple_pattern() {
let _g = crate::test_util::global_state_lock();
let first = crate::regex_module::zcond_regex_match(&["abc", "abcdef"], 0);
for _ in 0..5 {
assert_eq!(
crate::regex_module::zcond_regex_match(&["abc", "abcdef"], 0),
first,
"zcond_regex_match must be pure across calls"
);
}
}
#[test]
fn zcond_regex_match_result_is_boolean_only() {
let _g = crate::test_util::global_state_lock();
for pair in &[
(".", "a"),
("[0-9]+", "abc"),
("xyz", "abc"),
("^foo", "foobar"),
(".+", ""),
] {
let r = crate::regex_module::zcond_regex_match(&[pair.0, pair.1], 0);
assert!(
r == 0 || r == 1,
"regex match ({:?}, {:?}) → {} not in 0/1",
pair.0,
pair.1,
r
);
}
}
#[test]
fn zregex_regerrwarn_long_inputs_safe() {
let _g = crate::test_util::global_state_lock();
let long_prefix = "p".repeat(1000);
let long_msg = "m".repeat(2000);
crate::regex_module::zregex_regerrwarn(&long_prefix, &long_msg);
}
#[test]
fn zregex_regerrwarn_multibyte_and_ansi_safe() {
let _g = crate::test_util::global_state_lock();
crate::regex_module::zregex_regerrwarn("日本", "包含中文");
crate::regex_module::zregex_regerrwarn("\x1b[31m", "red message\x1b[0m");
}
#[test]
fn zcond_regex_match_malformed_pattern_no_panic() {
let _g = crate::test_util::global_state_lock();
for bad in &["[", "(", "*", "+", "(?", "[a-"] {
let _ = crate::regex_module::zcond_regex_match(&[bad, "input"], 0);
}
}
#[test]
fn regex_setup_returns_i32_type() {
let _g = crate::test_util::global_state_lock();
let _: i32 = crate::regex_module::setup_(std::ptr::null());
}
#[test]
fn regex_enables_with_none_returns_i32() {
let _g = crate::test_util::global_state_lock();
let mut e: Option<Vec<i32>> = None;
let _: i32 = crate::regex_module::enables_(std::ptr::null(), &mut e);
}
#[test]
fn regex_features_safe_with_caller_vec() {
let _g = crate::test_util::global_state_lock();
let mut v: Vec<String> = Vec::new();
let _ = crate::regex_module::features_(std::ptr::null(), &mut v);
}
#[test]
fn regex_each_lifecycle_hook_returns_zero_individually() {
let _g = crate::test_util::global_state_lock();
let null = std::ptr::null();
let mut v: Vec<String> = Vec::new();
let mut e: Option<Vec<i32>> = None;
assert_eq!(crate::regex_module::setup_(null), 0, "c:215 setup_");
assert_eq!(
crate::regex_module::features_(null, &mut v),
0,
"c:222 features_"
);
assert_eq!(
crate::regex_module::enables_(null, &mut e),
0,
"c:228 enables_"
);
assert_eq!(crate::regex_module::boot_(null), 0, "c:234 boot_");
assert_eq!(crate::regex_module::cleanup_(null), 0, "c:242 cleanup_");
assert_eq!(crate::regex_module::finish_(null), 0, "c:248 finish_");
}
#[test]
fn zcond_regex_match_single_arg_no_panic() {
let _g = crate::test_util::global_state_lock();
let _ = crate::regex_module::zcond_regex_match(&["pattern"], 0);
}
#[cfg(target_env = "gnu")]
#[test]
fn gnu_ere_word_escape_matches_glibc_regcomp() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
crate::regex_module::zcond_regex_match(
&["user@site.com", r"^(\w+)@(\w+)\.com$"],
ZREGEX_EXTENDED
),
1,
r"glibc \w = [[:alnum:]_]; ^(\w+)@(\w+)\.com$ must match user@site.com"
);
assert_eq!(
crate::regex_module::zcond_regex_match(&["-", r"^\w$"], ZREGEX_EXTENDED),
0,
r"'-' is not [[:alnum:]_]"
);
}
#[cfg(target_env = "gnu")]
#[test]
fn gnu_ere_remaining_operators_match_glibc_regcomp() {
let _g = crate::test_util::global_state_lock();
let saved = opt_state_get("casematch").unwrap_or(false);
opt_state_set("casematch", true);
let cases: &[(&str, &str, i32, &str)] = &[
("a-b", r"a\Wb", 1, r"\W = [^[:alnum:]_]"),
("a_b", r"a\Wb", 0, r"_ IS a word char, so \W must reject"),
("a b", r"a\sb", 1, r"\s = [[:space:]]"),
("a\tb", r"a\sb", 1, r"\s covers TAB"),
("ab", r"a\Sb", 0, r"\S needs a non-space between a and b"),
(
"foo bar",
r"\bbar",
1,
r"\b = WORD_FIRST|WORD_LAST boundary",
),
("foobar", r"\bbar", 0, r"no boundary inside foobar"),
("foobar", r"o\Bb", 1, r"\B = INSIDE_WORD|INSIDE_NOTWORD"),
("foo bar", r"\<bar", 1, r"\< = WORD_FIRST"),
("foobar", r"\<bar", 0, r"\< needs a not-word char before"),
("foo bar", r"bar\>", 1, r"\> = WORD_LAST"),
("barfoo", r"bar\>", 0, r"\> needs a not-word char after"),
("abc", "\\`abc", 1, r"\` = BUF_FIRST"),
("xabc", "\\`abc", 0, r"\` anchors at buffer start only"),
("abc", r"abc\'", 1, r"\' = BUF_LAST"),
("abcx", r"abc\'", 0, r"\' anchors at buffer end only"),
];
let observed: Vec<(&str, &str, i32)> = cases
.iter()
.map(|(s, p, _, _)| {
(
*s,
*p,
crate::regex_module::zcond_regex_match(&[s, p], ZREGEX_EXTENDED),
)
})
.collect();
opt_state_set("casematch", saved);
for ((s, p, want, why), (_, _, got)) in cases.iter().zip(observed) {
assert_eq!(got, *want, "[[ {:?} =~ {:?} ]] — {}", s, p, why);
}
}
#[cfg(not(target_env = "gnu"))]
#[test]
fn bsd_ere_gnu_operators_stay_literal() {
let _g = crate::test_util::global_state_lock();
let saved = opt_state_get("casematch").unwrap_or(false);
opt_state_set("casematch", true);
let cases: &[(&str, &str, i32, &str)] = &[
(
"user@site.com",
r"^(\w+)@(\w+)\.com$",
0,
r"\w is literal w",
),
("w", r"^\w$", 1, r"\w matches the LETTER w"),
("a b", r"a\sb", 0, r"\s is literal s"),
("asb", r"a\sb", 1, r"…so a\sb matches 'asb'"),
("foo bar", r"\bbar", 0, r"\b is literal b"),
("foo bbar", r"\bbar", 1, r"…so \bbar matches 'bbar'"),
(
"foo bar",
r"\<bar",
0,
r"\< is literal <, NOT a word boundary",
),
("foo <bar", r"\<bar", 1, r"…so \<bar matches '<bar'"),
("foo bar", r"bar\>", 0, r"\> is literal >"),
("bar> x", r"bar\>", 1, r"…so bar\> matches 'bar>'"),
(
"abab",
r"(ab)\1",
0,
r"\1 is literal 1, not a backreference",
),
];
let observed: Vec<(&str, &str, i32)> = cases
.iter()
.map(|(s, p, _, _)| {
(
*s,
*p,
crate::regex_module::zcond_regex_match(&[s, p], ZREGEX_EXTENDED),
)
})
.collect();
opt_state_set("casematch", saved);
for ((s, p, want, why), (_, _, got)) in cases.iter().zip(observed) {
assert_eq!(got, *want, "[[ {:?} =~ {:?} ]] — {}", s, p, why);
}
}
#[test]
fn digit_escape_is_literal_on_every_libc() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
crate::regex_module::zcond_regex_match(
&["2024-06", r"(\d{2,4})-(\d{2})"],
ZREGEX_EXTENDED
),
0,
r"\d is a literal 'd' in POSIX ERE on both glibc and BSD"
);
assert_eq!(
crate::regex_module::zcond_regex_match(
&["dd-dd", r"(\d{2,4})-(\d{2})"],
ZREGEX_EXTENDED
),
1,
r"…so the pattern matches a run of literal 'd's instead of digits"
);
}
#[test]
fn gnu_operators_do_not_leak_into_bracket_expressions() {
let _g = crate::test_util::global_state_lock();
let translated = crate::regex_module::posix_ere_bracket_escape(r"[\w]");
assert_eq!(
translated, r"[\\w]",
"backslash inside a list is a literal member on every libc"
);
assert_eq!(
crate::regex_module::zcond_regex_match(&["5", r"^[\w]$"], ZREGEX_EXTENDED),
0,
r"[\w] must not become a digit-accepting class"
);
assert_eq!(
crate::regex_module::zcond_regex_match(&["\\", r"^[\w]$"], ZREGEX_EXTENDED),
1,
r"[\w] is the two-member set: a backslash, and the letter w"
);
}
}
pub fn posix_ere_bracket_escape(pat: &str) -> String {
let chars: Vec<char> = pat.chars().collect();
let mut out = String::with_capacity(pat.len() + 8);
let mut i = 0;
const ERE_SPECIAL: &[char] = &[
'.', '[', ']', '\\', '(', ')', '*', '+', '?', '{', '}', '|', '^', '$',
];
const RE2_SYNTAX_PUNCT: &[char] = &['<', '>'];
while i < chars.len() {
let c = chars[i];
if c == '\\' {
i += 1;
if i < chars.len() {
let n = chars[i];
#[cfg(target_env = "gnu")]
let gnu_op: Option<&'static str> = match n {
'w' => Some("[[:alnum:]_]"),
'W' => Some("[^[:alnum:]_]"),
's' => Some("[[:space:]]"),
'S' => Some("[^[:space:]]"),
'b' => Some(r"(?-u:\b)"),
'B' => Some(r"(?-u:\B)"),
'<' => Some(r"(?-u:\b{start})"),
'>' => Some(r"(?-u:\b{end})"),
'`' => Some(r"\A"),
'\'' => Some(r"\z"),
_ => None,
};
#[cfg(not(target_env = "gnu"))]
let gnu_op: Option<&'static str> = None;
if let Some(gnu) = gnu_op {
out.push_str(gnu);
} else if ERE_SPECIAL.contains(&n) {
out.push('\\');
out.push(n);
} else {
if n.is_alphanumeric() || RE2_SYNTAX_PUNCT.contains(&n) {
out.push(n);
} else {
out.push('\\');
out.push(n);
}
}
i += 1;
} else {
out.push('\\');
}
continue;
}
if c != '[' {
out.push(c);
i += 1;
continue;
}
out.push('[');
i += 1;
if i < chars.len() && chars[i] == '^' {
out.push('^');
i += 1;
}
if i < chars.len() && chars[i] == ']' {
out.push('\\');
out.push(']');
i += 1;
}
while i < chars.len() && chars[i] != ']' {
if chars[i] == '[' && i + 1 < chars.len() && matches!(chars[i + 1], ':' | '=' | '.') {
let close = chars[i + 1];
out.push('[');
out.push(close);
i += 2;
while i < chars.len() && !(chars[i] == close && chars.get(i + 1) == Some(&']')) {
out.push(chars[i]);
i += 1;
}
if i < chars.len() {
out.push(close);
out.push(']');
i += 2;
}
continue;
}
if matches!(chars[i], '\\' | '[' | '&' | '~') {
out.push('\\');
}
out.push(chars[i]);
i += 1;
}
if i < chars.len() {
out.push(']'); i += 1;
}
}
out
}