#![allow(non_upper_case_globals)]
#![allow(non_camel_case_types)]
use std::sync::atomic::{AtomicI32, AtomicUsize, Ordering};
use std::sync::Mutex;
pub use crate::ported::zsh_h::{
patstralloc, GF_BACKREF, GF_IGNCASE, GF_LCMATCHUC, GF_MATCHREF, GF_MULTIBYTE, PAT_ANY,
PAT_FILE, PAT_FILET, PAT_HAS_EXCLUDP, PAT_HEAPDUP, PAT_LCMATCHUC, PAT_NOANCH, PAT_NOGLD,
PAT_NOTEND, PAT_NOTSTART, PAT_PURES, PAT_SCAN, PAT_STATIC, PAT_ZDUP, Patstralloc,
};
use crate::ported::params::{paramtab, paramtab_hashed_storage};
use crate::ported::utils::ztrsub;
use crate::ported::zle::zle_h::{COMP_LIST_COMPLETE, COMP_LIST_EXPAND};
use crate::utils::zerrnam;
use crate::zsh_h::{Bang, Bar, Hat, Inang, Inbrack, Inpar, Marker, Meta, Nularg, Outbrack, Pound, Quest, Star,
isset, patprog, BASHAUTOLIST, CASEGLOB, CASEPATHS, EXTENDEDGLOB, KSHGLOB,
MULTIBYTE, NUMERICGLOBSORT, PM_HASHED, PM_TYPE, RCQUOTES, SHGLOB, SORTIT_IGNORING_BACKSLASHES,
SORTIT_NUMERICALLY, ZPC_BAR, ZPC_BNULLKEEP, ZPC_COUNT, ZPC_HASH,
ZPC_HAT, ZPC_INANG, ZPC_INBRACK, ZPC_INPAR,
ZPC_KSH_AT, ZPC_KSH_BANG, ZPC_KSH_BANG2, ZPC_KSH_PLUS, ZPC_KSH_QUEST,
ZPC_KSH_STAR, ZPC_NULL, ZPC_OUTPAR, ZPC_QUEST, ZPC_SLASH, ZPC_STAR, ZPC_TILDE,
};
pub const NSUBEXP: usize = 9;
pub const P_END: u8 = 0x00; pub const P_EXCSYNC: u8 = 0x01; pub const P_EXCEND: u8 = 0x02; pub const P_BACK: u8 = 0x03; pub const P_EXACTLY: u8 = 0x04; pub const P_NOTHING: u8 = 0x05; pub const P_ONEHASH: u8 = 0x06; pub const P_TWOHASH: u8 = 0x07; pub const P_GFLAGS: u8 = 0x08; pub const P_ISSTART: u8 = 0x09; pub const P_ISEND: u8 = 0x0a; pub const P_COUNTSTART: u8 = 0x0b; pub const P_COUNT: u8 = 0x0c; pub const P_BRANCH: u8 = 0x20; pub const P_WBRANCH: u8 = 0x21; pub const P_EXCLUDE: u8 = 0x30; pub const P_EXCLUDP: u8 = 0x31; pub const P_ANY: u8 = 0x40; pub const P_ANYOF: u8 = 0x41; pub const P_ANYBUT: u8 = 0x42; pub const P_STAR: u8 = 0x43; pub const P_NUMRNG: u8 = 0x44; pub const P_NUMFROM: u8 = 0x45; pub const P_NUMTO: u8 = 0x46; pub const P_NUMANY: u8 = 0x47; pub const P_OPEN: u8 = 0x80; pub const P_CLOSE: u8 = 0x90;
#[inline]
pub fn P_ISBRANCH(op: u8) -> bool {
(op & 0x20) != 0
}
#[inline]
pub fn P_ISEXCLUDE(op: u8) -> bool {
(op & 0x30) == 0x30
}
#[inline]
pub fn P_NOTDOT(op: u8) -> bool {
(op & 0x40) != 0
}
pub const P_SIMPLE: i32 = 0x01; pub const P_HSTART: i32 = 0x02; pub const P_PURESTR: i32 = 0x04;
pub static patnpar: AtomicI32 = AtomicI32::new(0);
pub static patout: Mutex<Vec<u8>> = Mutex::new(Vec::new());
pub static patflags: AtomicI32 = AtomicI32::new(0);
pub static patglobflags: AtomicI32 = AtomicI32::new(0);
pub fn clear_shiftstate() {}
pub fn metacharinc(s: &str, pos: usize) -> usize {
s[pos..]
.chars()
.next()
.map(|c| pos + c.len_utf8())
.unwrap_or(pos)
}
pub const PA_NOALIGN: i32 = 1; pub const PA_UNMETA: i32 = 2;
fn patadd(add: Option<&[u8]>, ch: u8, n: i64, paflags: i32) {
use crate::ported::lex::ztokens as ztokens_str;
use crate::ported::ztype_h::itok;
use crate::ported::zsh_h::Pound;
let ztokens = ztokens_str.as_bytes();
let mut buf = patout.lock().unwrap();
let patsize = buf.len() as i64;
let mut newpatsize = patsize + n;
if (paflags & PA_NOALIGN) == 0 {
let upat = 8i64; newpatsize = (newpatsize + upat - 1) & !(upat - 1); }
if newpatsize > buf.len() as i64 {
buf.resize(newpatsize as usize, 0);
}
let mut patcode_pos = patsize as usize;
if let Some(add_bytes) = add {
if (paflags & PA_UNMETA) != 0 {
let mut idx = 0usize;
let mut remaining = n;
while remaining > 0 && idx < add_bytes.len() {
let b = add_bytes[idx];
if itok(b) {
if idx < add_bytes.len() {
let tok_idx = b.wrapping_sub(Pound as u8) as usize;
if tok_idx < ztokens.len() {
if patcode_pos < buf.len() {
buf[patcode_pos] = ztokens[tok_idx];
}
patcode_pos += 1;
}
idx += 1;
}
} else if b == Meta {
idx += 1;
if idx < add_bytes.len() {
if patcode_pos < buf.len() {
buf[patcode_pos] = add_bytes[idx] ^ 32;
}
patcode_pos += 1;
idx += 1;
}
} else {
if patcode_pos < buf.len() {
buf[patcode_pos] = b;
}
patcode_pos += 1;
idx += 1;
}
remaining -= 1;
}
} else {
let n_actual = (n as usize).min(add_bytes.len());
for &b in &add_bytes[..n_actual] {
if patcode_pos < buf.len() {
buf[patcode_pos] = b;
}
patcode_pos += 1;
}
}
} else {
if patcode_pos < buf.len() {
buf[patcode_pos] = ch;
}
patcode_pos += 1;
}
let _ = patcode_pos;
}
pub fn patcompcharsset() {
let mut sp = zpc_special.lock().unwrap();
*sp = [0u8; ZPC_COUNT as usize];
sp[ZPC_SLASH as usize] = b'/';
sp[ZPC_NULL as usize] = 0;
sp[ZPC_BAR as usize] = b'|';
sp[ZPC_OUTPAR as usize] = b')';
sp[ZPC_TILDE as usize] = b'~';
sp[ZPC_INPAR as usize] = b'(';
sp[ZPC_QUEST as usize] = b'?';
sp[ZPC_STAR as usize] = b'*';
sp[ZPC_INBRACK as usize] = b'[';
sp[ZPC_INANG as usize] = b'<';
sp[ZPC_HAT as usize] = b'^';
sp[ZPC_HASH as usize] = b'#';
sp[ZPC_BNULLKEEP as usize] = 0;
sp[ZPC_KSH_QUEST as usize] = b'?';
sp[ZPC_KSH_STAR as usize] = b'*';
sp[ZPC_KSH_PLUS as usize] = b'+';
sp[ZPC_KSH_BANG as usize] = b'!';
sp[ZPC_KSH_BANG2 as usize] = b'!';
sp[ZPC_KSH_AT as usize] = b'@';
let marker_byte = Marker as u32 as u8;
{
let disp = zpc_disables.lock().unwrap();
for i in 0..(ZPC_COUNT as usize) {
if disp[i] != 0 {
sp[i] = marker_byte; }
}
}
if !isset(EXTENDEDGLOB) {
sp[ZPC_TILDE as usize] = marker_byte;
sp[ZPC_HAT as usize] = marker_byte;
sp[ZPC_HASH as usize] = marker_byte;
}
if !isset(KSHGLOB) {
sp[ZPC_KSH_QUEST as usize] = marker_byte;
sp[ZPC_KSH_STAR as usize] = marker_byte;
sp[ZPC_KSH_PLUS as usize] = marker_byte;
sp[ZPC_KSH_BANG as usize] = marker_byte;
sp[ZPC_KSH_BANG2 as usize] = marker_byte;
sp[ZPC_KSH_AT as usize] = marker_byte;
}
if isset(SHGLOB) {
sp[ZPC_INPAR as usize] = marker_byte;
sp[ZPC_INANG as usize] = marker_byte;
}
}
pub fn patcompstart() {
patcompcharsset();
patout.lock().unwrap().clear();
patnpar.store(1, Ordering::Relaxed);
patflags.store(0, Ordering::Relaxed);
let mut flags: i32 = if isset(CASEGLOB) || isset(CASEPATHS) {
0 } else {
GF_IGNCASE };
if isset(MULTIBYTE) {
flags |= GF_MULTIBYTE; }
patglobflags.store(flags, Ordering::Relaxed);
errsfound.store(0, Ordering::Relaxed);
forceerrs.store(-1, Ordering::Relaxed);
patparse_off.store(0, Ordering::Relaxed);
}
pub fn patcompile(exp: &str, inflags: i32, mut endexp: Option<&mut String>) -> Option<Patprog> {
let _compile_guard = PATCOMPILE_LOCK.lock().unwrap_or_else(|e| e.into_inner());
patcompstart();
*patstart.lock().unwrap() = exp.to_string();
*patparse.lock().unwrap() = exp.to_string();
patflags.store(
inflags & !(PAT_PURES | PAT_HAS_EXCLUDP) as i32,
Ordering::Relaxed,
); patglobflags.store(0, Ordering::Relaxed);
let mut hoisted_globflags: i32 = GF_MULTIBYTE; let hash_char_pre = zpc_special.lock().unwrap()[ZPC_HASH as usize]; while hash_char_pre == b'#' {
let off = patparse_off.load(Ordering::Relaxed);
let p = patparse.lock().unwrap();
if off + 1 >= p.len() || &p.as_bytes()[off..off + 2] != b"(#" {
break;
}
let rest = p[off..].to_string();
drop(p);
match patgetglobflags(&rest) {
Some((bits, _assert, consumed)) => {
hoisted_globflags |=
bits & (GF_IGNCASE | GF_LCMATCHUC | GF_MULTIBYTE | GF_BACKREF | GF_MATCHREF);
let errs_byte = bits & 0xff;
if errs_byte != 0 {
hoisted_globflags = (hoisted_globflags & !0xff) | errs_byte; }
if (bits & GF_MULTIBYTE) == 0 && rest.contains('U') {
hoisted_globflags &= !GF_MULTIBYTE;
}
patparse_off.fetch_add(consumed, Ordering::Relaxed);
}
None => break,
}
}
let mut flagp: i32 = 0;
let root = patcompswitch(0, &mut flagp);
if root < 0 {
return None; }
let end_off = patnode(P_END);
chain_branches_to(root as usize, end_off);
let code = patout.lock().unwrap().clone();
let consumed_off = patparse_off.load(Ordering::Relaxed);
if let Some(end) = endexp.as_deref_mut() {
let parse = patparse.lock().unwrap();
*end = parse[consumed_off..].to_string();
}
Some(Box::new((
patprog {
startoff: 0,
size: code.len() as i64,
mustoff: 0,
patmlen: 0,
globflags: hoisted_globflags,
globend: patglobflags.load(Ordering::Relaxed),
flags: patflags.load(Ordering::Relaxed),
patnpar: patnpar.load(Ordering::Relaxed) - 1,
patstartch: 0,
},
code,
)))
}
pub fn patcompswitch(paren: i32, flagp: &mut i32) -> i64 {
let starter = patnode(P_BRANCH);
let mut branch_flags: i32 = 0;
let first_branch = patcompbranch(&mut branch_flags, paren);
if first_branch < 0 {
return -1;
}
*flagp |= branch_flags & P_HSTART;
let mut last_branch = starter;
loop {
let off = patparse_off.load(Ordering::Relaxed);
let parse = patparse.lock().unwrap();
if off >= parse.len() {
break;
}
let c = parse.as_bytes()[off];
if c != b'|' {
break;
}
drop(parse);
patparse_off.fetch_add(1, Ordering::Relaxed);
let br = patnode(P_BRANCH);
set_next(last_branch, br);
let mut bf: i32 = 0;
let inner = patcompbranch(&mut bf, paren);
if inner < 0 {
return -1;
}
*flagp |= bf & P_HSTART;
last_branch = br;
}
let _ = first_branch;
starter as i64
}
pub fn patcompbranch(flagp: &mut i32, paren: i32) -> i64 {
let mut chain_start: i64 = -1;
let mut last_tail: usize = 0;
*flagp = P_PURESTR;
loop {
let off = patparse_off.load(Ordering::Relaxed);
let snapshot: Vec<u8> = {
let parse = patparse.lock().unwrap();
parse.as_bytes().to_vec()
};
if off >= snapshot.len() {
break;
}
let c = snapshot[off];
if c == b'|' || c == b')' {
break;
}
let bytes = snapshot.as_slice();
if off + 2 < bytes.len()
&& bytes[off] == b'('
&& bytes[off + 1] == b'#'
&& bytes[off + 2] == b'c'
{
let mut j = off + 3;
let mut min: i64 = 0;
let min_start = j;
while j < bytes.len() && bytes[j].is_ascii_digit() {
min = min * 10 + (bytes[j] - b'0') as i64;
j += 1;
}
let mut max: i64 = i64::MAX;
if j > min_start {
if j < bytes.len() && bytes[j] == b',' {
j += 1;
let max_start = j;
let mut mx: i64 = 0;
while j < bytes.len() && bytes[j].is_ascii_digit() {
mx = mx * 10 + (bytes[j] - b'0') as i64;
j += 1;
}
if j > max_start {
max = mx;
}
} else {
max = min;
}
if j < bytes.len() && bytes[j] == b')' {
j += 1;
patparse_off.store(j, Ordering::Relaxed);
let count_off = patnode(P_COUNT);
let mut buf = patout.lock().unwrap();
buf.extend_from_slice(&min.to_le_bytes());
buf.extend_from_slice(&max.to_le_bytes());
drop(buf);
let mut piece_flags: i32 = 0;
let mut piece_tail: usize = 0;
let piece = patcomppiece(&mut piece_flags, paren, &mut piece_tail);
if piece < 0 {
return -1;
}
set_next(piece_tail, 0);
if chain_start < 0 {
chain_start = count_off as i64;
} else {
set_next(last_tail, count_off);
}
last_tail = count_off;
continue;
}
}
}
let hash_char = zpc_special.lock().unwrap()[ZPC_HASH as usize]; if hash_char == b'#'
&& off + 1 < bytes.len()
&& bytes[off] == b'('
&& bytes[off + 1] == b'#'
{
let rest = std::str::from_utf8(&bytes[off..]).unwrap_or("").to_string();
if let Some((bits, assertp, consumed)) = patgetglobflags(&rest) {
patparse_off.fetch_add(consumed, Ordering::Relaxed);
let flag_bits = bits & (GF_IGNCASE | GF_LCMATCHUC | GF_MULTIBYTE);
if flag_bits != 0 || (flag_bits == 0 && assertp == 0) {
let gf_off = patnode(P_GFLAGS);
let mut buf = patout.lock().unwrap();
buf.extend_from_slice(&flag_bits.to_le_bytes());
drop(buf);
if chain_start < 0 {
chain_start = gf_off as i64;
} else {
set_next(last_tail, gf_off);
}
last_tail = gf_off;
}
if assertp != 0 {
let as_off = patnode(assertp as u8);
if chain_start < 0 {
chain_start = as_off as i64;
} else {
set_next(last_tail, as_off);
}
last_tail = as_off;
}
continue;
}
}
let mut piece_flags: i32 = 0;
let mut piece_tail: usize = 0;
let piece = patcomppiece(&mut piece_flags, paren, &mut piece_tail);
if piece < 0 {
return -1;
}
if chain_start < 0 {
chain_start = piece;
} else {
set_next(last_tail, piece as usize);
}
last_tail = piece_tail;
*flagp &= piece_flags;
}
if chain_start < 0 {
chain_start = patnode(P_NOTHING) as i64;
}
chain_start
}
pub fn patgetglobflags(s: &str) -> Option<(i32, i64, usize)> {
let bytes = s.as_bytes();
if !s.starts_with("(#") {
return None;
}
let mut i = 2;
let mut bits: i32 = 0;
let mut assertp: i64 = 0;
while i < bytes.len() && bytes[i] != b')' {
match bytes[i] {
b'i' => {
bits |= GF_IGNCASE;
bits &= !GF_LCMATCHUC;
i += 1;
}
b'I' => {
bits &= !(GF_LCMATCHUC | GF_IGNCASE);
i += 1;
} b'l' => {
bits |= GF_LCMATCHUC;
bits &= !GF_IGNCASE;
i += 1;
}
b'b' => {
bits |= GF_BACKREF;
i += 1;
} b'B' => {
bits &= !GF_BACKREF;
i += 1;
} b'm' => {
bits |= GF_MATCHREF;
i += 1;
} b'M' => {
bits &= !GF_MATCHREF;
i += 1;
} b's' => {
assertp = P_ISSTART as i64;
i += 1;
} b'e' => {
assertp = P_ISEND as i64;
i += 1;
} b'u' => {
bits |= GF_MULTIBYTE;
i += 1;
} b'U' => {
bits &= !GF_MULTIBYTE;
i += 1;
} b'a' => {
i += 1;
let digit_start = i;
let mut errs: i32 = 0;
while i < bytes.len() && bytes[i].is_ascii_digit() {
errs = errs * 10 + (bytes[i] - b'0') as i32;
i += 1;
}
if i == digit_start {
return None;
}
if errs < 0 || errs > 254 {
return None;
} bits = (bits & !0xff) | (errs & 0xff); }
b'q' => {
while i < bytes.len() && bytes[i] != b')' {
i += 1;
}
}
_ => return None,
}
}
if i >= bytes.len() {
return None;
}
i += 1; Some((bits, assertp, i))
}
pub fn range_type(start: &str) -> Option<usize> {
POSIX_CLASS_NAMES
.iter()
.position(|n| *n == start)
.map(|i| i + 1)
}
pub fn pattern_range_to_string(rangestr: &str) -> String {
let mut out = String::with_capacity(rangestr.len()); let mut chars = rangestr.chars().peekable();
while let Some(c) = chars.next() {
if c == '[' && chars.peek() == Some(&':') {
let mut name = String::new();
chars.next(); while let Some(&cc) = chars.peek() {
if cc == ':' {
chars.next();
if chars.peek() == Some(&']') {
chars.next();
break;
}
name.push(':');
} else {
name.push(cc);
chars.next();
}
}
out.push_str(&format!("[:{}:]", name)); } else {
out.push(c); if chars.peek() == Some(&'-') {
chars.next();
if let Some(c2) = chars.next() {
out.push('-'); out.push(c2); }
}
}
}
out }
pub fn patcomppiece(flagp: &mut i32, paren: i32, tail_out: &mut usize) -> i64 {
let _ = paren;
let off = patparse_off.load(Ordering::Relaxed);
let parse = patparse.lock().unwrap();
if off >= parse.len() {
return patnode(P_NOTHING) as i64;
}
let bytes = parse.as_bytes();
let c = bytes[off];
let mut kshchar: u8 = 0; if off + 1 < parse.len() && bytes[off + 1] == b'(' {
let sp = zpc_special.lock().unwrap();
if c == sp[ZPC_KSH_PLUS as usize] {
kshchar = b'+';
}
else if c == sp[ZPC_KSH_BANG as usize] {
kshchar = b'!';
}
else if c == sp[ZPC_KSH_BANG2 as usize] {
kshchar = b'!';
}
else if c == sp[ZPC_KSH_AT as usize] {
kshchar = b'@';
}
else if c == sp[ZPC_KSH_STAR as usize] {
kshchar = b'*';
}
else if c == sp[ZPC_KSH_QUEST as usize] {
kshchar = b'?';
} }
drop(parse);
let dispatch_c = if kshchar != 0 {
patparse_off.fetch_add(1, Ordering::Relaxed);
b'(' } else {
c
};
let atom = match dispatch_c {
b'?' => {
patparse_off.fetch_add(1, Ordering::Relaxed);
*flagp |= P_SIMPLE;
*flagp &= !P_PURESTR;
let h = patnode(P_ANY);
*tail_out = h;
h as i64
}
b'*' => {
patparse_off.fetch_add(1, Ordering::Relaxed);
*flagp &= !P_PURESTR;
let h = patnode(P_STAR);
*tail_out = h;
h as i64
}
b'[' => {
patparse_off.fetch_add(1, Ordering::Relaxed);
*flagp |= P_SIMPLE;
*flagp &= !P_PURESTR;
let mut chars: Vec<u8> = Vec::new();
let mut negate = false;
let bracket_start = patparse_off.load(Ordering::Relaxed);
let parse_b = patparse.lock().unwrap();
let bb = parse_b.as_bytes();
let mut i_b = bracket_start;
if i_b < bb.len() && (bb[i_b] == b'^' || bb[i_b] == b'!') {
negate = true;
i_b += 1;
}
while i_b < bb.len() && bb[i_b] != b']' {
if i_b + 1 < bb.len() && bb[i_b] == b'[' && bb[i_b + 1] == b':' {
let class_start = i_b + 2;
let mut j_b = class_start;
while j_b + 1 < bb.len() && !(bb[j_b] == b':' && bb[j_b + 1] == b']') {
j_b += 1;
}
if j_b + 1 < bb.len() {
let class_name = std::str::from_utf8(&bb[class_start..j_b]).unwrap_or("");
match class_name {
"alpha" => {
for c in b'a'..=b'z' {
chars.push(c);
}
for c in b'A'..=b'Z' {
chars.push(c);
}
}
"upper" => {
for c in b'A'..=b'Z' {
chars.push(c);
}
}
"lower" => {
for c in b'a'..=b'z' {
chars.push(c);
}
}
"digit" => {
for c in b'0'..=b'9' {
chars.push(c);
}
}
"xdigit" => {
for c in b'0'..=b'9' {
chars.push(c);
}
for c in b'a'..=b'f' {
chars.push(c);
}
for c in b'A'..=b'F' {
chars.push(c);
}
}
"alnum" => {
for c in b'a'..=b'z' {
chars.push(c);
}
for c in b'A'..=b'Z' {
chars.push(c);
}
for c in b'0'..=b'9' {
chars.push(c);
}
}
"space" => {
for b in b" \t\n\r\x0b\x0c".iter() {
chars.push(*b);
}
}
"blank" => {
chars.push(b' ');
chars.push(b'\t');
}
"punct" => {
for b in b"!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~".iter() {
chars.push(*b);
}
}
"cntrl" => {
for c in 0u8..=31 {
chars.push(c);
}
chars.push(127);
}
"print" => {
for c in 32u8..=126 {
chars.push(c);
}
}
"graph" => {
for c in 33u8..=126 {
chars.push(c);
}
}
_ => {}
}
i_b = j_b + 2;
continue;
}
}
if i_b + 2 < bb.len() && bb[i_b + 1] == b'-' && bb[i_b + 2] != b']' {
let lo = bb[i_b];
let hi = bb[i_b + 2];
for c in lo..=hi {
chars.push(c);
}
i_b += 3;
} else {
chars.push(bb[i_b]);
i_b += 1;
}
}
drop(parse_b);
if let Some(p_lock) = patparse.lock().ok() {
if i_b < p_lock.len() && p_lock.as_bytes()[i_b] == b']' {
i_b += 1;
}
}
patparse_off.store(i_b, Ordering::Relaxed);
let opcode = if negate { P_ANYBUT } else { P_ANYOF };
let off2 = patnode(opcode);
let mut buf = patout.lock().unwrap();
let len = chars.len() as u32;
buf.extend_from_slice(&len.to_le_bytes());
buf.extend_from_slice(&chars);
*tail_out = off2;
off2 as i64
}
b'(' => {
patparse_off.fetch_add(1, Ordering::Relaxed);
*flagp &= !P_PURESTR;
if kshchar == b'!' {
let mut flags2: i32 = 0;
let starter_off = patcompnot(1, &mut flags2);
if starter_off < 0 {
return -1;
}
*flagp |= flags2 & P_HSTART; *tail_out = starter_off as usize;
return starter_off;
}
let n = patnpar.fetch_add(1, Ordering::Relaxed);
if n >= NSUBEXP as i32 {
return -1;
}
let opcode = P_OPEN + n as u8;
let open_off = patnode(opcode);
let mut inner_flags: i32 = 0;
let inner = patcompswitch(1, &mut inner_flags);
if inner < 0 {
return -1;
}
let cur_off = patparse_off.load(Ordering::Relaxed);
let p = patparse.lock().unwrap();
if cur_off >= p.len() || p.as_bytes()[cur_off] != b')' {
return -1;
}
drop(p);
patparse_off.fetch_add(1, Ordering::Relaxed);
let close_off = patnode(P_CLOSE + n as u8);
set_next(open_off, inner as usize);
pattail(open_off, close_off);
chain_branches_to(inner as usize, close_off);
*flagp &= !P_PURESTR;
*tail_out = close_off;
open_off as i64
}
b'\\' => {
patparse_off.fetch_add(1, Ordering::Relaxed);
let p = patparse.lock().unwrap();
let off2 = patparse_off.load(Ordering::Relaxed);
if off2 >= p.len() {
return -1;
}
let escaped = p.as_bytes()[off2];
drop(p);
patparse_off.fetch_add(1, Ordering::Relaxed);
*flagp |= P_SIMPLE;
let lit_off = patnode(P_EXACTLY);
let mut buf_lit = patout.lock().unwrap();
buf_lit.extend_from_slice(&1u32.to_le_bytes());
buf_lit.push(escaped);
*tail_out = lit_off;
lit_off as i64
}
b'<' => {
patparse_off.fetch_add(1, Ordering::Relaxed);
*flagp &= !P_PURESTR;
let parse_n = patparse.lock().unwrap();
let nb = parse_n.as_bytes();
let mut j = patparse_off.load(Ordering::Relaxed);
let mut len_flag: u8 = 0; let mut from: i64 = 0;
let lo_start = j;
while j < nb.len() && nb[j].is_ascii_digit() {
from = from * 10 + (nb[j] - b'0') as i64;
j += 1;
}
if j > lo_start {
len_flag |= 1;
} if j >= nb.len() || nb[j] != b'-' {
drop(parse_n);
return -1;
}
j += 1; let mut to: i64 = 0;
let hi_start = j;
while j < nb.len() && nb[j].is_ascii_digit() {
to = to * 10 + (nb[j] - b'0') as i64;
j += 1;
}
if j > hi_start {
len_flag |= 2;
} if j >= nb.len() || nb[j] != b'>' {
drop(parse_n);
return -1; }
j += 1;
drop(parse_n);
patparse_off.store(j, Ordering::Relaxed);
let off2 = match len_flag {
3 => {
let off2 = patnode(P_NUMRNG);
let mut buf = patout.lock().unwrap();
buf.extend_from_slice(&from.to_le_bytes());
buf.extend_from_slice(&to.to_le_bytes());
off2
}
2 => {
let off2 = patnode(P_NUMTO);
let mut buf = patout.lock().unwrap();
buf.extend_from_slice(&to.to_le_bytes());
off2
}
1 => {
let off2 = patnode(P_NUMFROM);
let mut buf = patout.lock().unwrap();
buf.extend_from_slice(&from.to_le_bytes());
off2
}
_ => patnode(P_NUMANY), };
*tail_out = off2;
off2 as i64
}
_ => {
let mut buf: Vec<u8> = Vec::new();
let mut local_off = off;
let p = patparse.lock().unwrap();
let (ksh_plus, ksh_bang, ksh_bang2, ksh_at, ksh_star, ksh_quest) = {
let sp = zpc_special.lock().unwrap();
(
sp[ZPC_KSH_PLUS as usize],
sp[ZPC_KSH_BANG as usize],
sp[ZPC_KSH_BANG2 as usize],
sp[ZPC_KSH_AT as usize],
sp[ZPC_KSH_STAR as usize],
sp[ZPC_KSH_QUEST as usize],
)
};
while local_off < p.len() {
let b = p.as_bytes()[local_off];
if matches!(
b,
b'?' | b'*' | b'[' | b'(' | b')' | b'|' | b'\\' | b'#' | b'^' | b'<'
) {
break;
}
if local_off + 1 < p.len() && p.as_bytes()[local_off + 1] == b'(' {
if b == ksh_plus
|| b == ksh_bang
|| b == ksh_bang2
|| b == ksh_at
|| b == ksh_star
|| b == ksh_quest
{
break;
}
}
buf.push(b);
local_off += 1;
}
drop(p);
if buf.is_empty() {
return -1;
}
patparse_off.store(local_off, Ordering::Relaxed);
*flagp |= P_SIMPLE;
let lit_off = patnode(P_EXACTLY);
let mut buf_lit = patout.lock().unwrap();
let len = buf.len() as u32;
buf_lit.extend_from_slice(&len.to_le_bytes());
buf_lit.extend_from_slice(&buf);
*tail_out = lit_off;
lit_off as i64
}
};
if atom < 0 {
return atom;
}
let q_off = patparse_off.load(Ordering::Relaxed);
let parse2 = patparse.lock().unwrap();
let hash_char = zpc_special.lock().unwrap()[ZPC_HASH as usize]; let has_hash = hash_char == b'#'
&& q_off < parse2.len()
&& parse2.as_bytes()[q_off] == b'#'; drop(parse2);
if !has_hash && (kshchar == 0 || kshchar == b'@' || kshchar == b'!') {
return atom; }
if kshchar != 0 && has_hash {
return -1;
}
let op: u8;
let mut consume_hashes = 0;
if kshchar == b'*' {
op = P_ONEHASH;
*flagp = P_HSTART;
} else if kshchar == b'+' {
op = P_TWOHASH;
*flagp = P_HSTART;
} else if kshchar == b'?' {
op = 0; *flagp = 0;
} else {
let parse_h = patparse.lock().unwrap();
let two = q_off + 1 < parse_h.len() && parse_h.as_bytes()[q_off + 1] == b'#';
drop(parse_h);
if two {
op = P_TWOHASH;
consume_hashes = 2;
} else {
op = P_ONEHASH;
consume_hashes = 1;
}
*flagp = P_HSTART;
}
if consume_hashes > 0 {
patparse_off.fetch_add(consume_hashes, Ordering::Relaxed);
}
let atom_op = {
let buf = patout.lock().unwrap();
if (atom as usize) + I_OP < buf.len() {
buf[atom as usize + I_OP]
} else {
0
}
};
if ((*flagp & P_SIMPLE) != 0)
&& (op == P_ONEHASH || op == P_TWOHASH)
&& atom_op == P_ANY
{
let mut buf = patout.lock().unwrap();
if op == P_TWOHASH {
drop(buf);
let _star = patnode(P_STAR);
*tail_out = atom as usize;
} else {
buf[atom as usize + I_OP] = P_STAR;
drop(buf);
*tail_out = atom as usize;
}
*flagp &= !P_PURESTR;
} else if ((*flagp & P_SIMPLE) != 0) && op != 0 && (patglobflags.load(Ordering::Relaxed) & 0xff) == 0 {
patinsert(op, atom as usize, None, 0);
*flagp &= !P_PURESTR;
*tail_out = atom as usize;
} else if op == P_ONEHASH {
let payload = [0u8; 8];
patinsert(P_WBRANCH, atom as usize, Some(&payload), 8);
let back = patnode(P_BACK);
patoptail(atom as usize, back); patoptail(atom as usize, atom as usize); let alt = patnode(P_BRANCH);
pattail(atom as usize, alt); let null_node = patnode(P_NOTHING);
pattail(atom as usize, null_node); *flagp &= !P_PURESTR;
*tail_out = null_node;
} else if op == P_TWOHASH {
let wbranch = patnode(P_WBRANCH); let payload = [0u8; 8]; {
let mut buf = patout.lock().unwrap();
buf.extend_from_slice(&payload);
}
pattail(atom as usize, wbranch); let back = patnode(P_BACK);
pattail(back, atom as usize); let alt = patnode(P_BRANCH);
pattail(wbranch, alt); let null_node = patnode(P_NOTHING);
pattail(atom as usize, null_node); *flagp &= !P_PURESTR;
*tail_out = null_node;
} else if kshchar == b'?' {
patinsert(P_BRANCH, atom as usize, None, 0); let alt = patnode(P_BRANCH); pattail(atom as usize, alt);
let null_node = patnode(P_NOTHING); pattail(atom as usize, null_node); patoptail(atom as usize, null_node); *flagp &= !P_PURESTR;
*tail_out = null_node;
}
{
let p = patparse.lock().unwrap();
let q2 = patparse_off.load(Ordering::Relaxed);
if q2 < p.len() && p.as_bytes()[q2] == b'#' {
let sp = zpc_special.lock().unwrap();
if sp[ZPC_HASH as usize] == b'#' {
return -1;
}
}
}
atom
}
pub fn patcompnot(paren: i32, flagsp: &mut i32) -> i64 {
*flagsp = P_HSTART;
let starter = patnode(P_BRANCH);
let br = patnode(P_STAR);
let excsync = patnode(P_EXCSYNC);
pattail(br, excsync);
let excl = patnode(P_EXCLUDE);
pattail(starter, excl);
{
let mut buf = patout.lock().unwrap();
buf.extend_from_slice(&[0u8; 8]);
}
let mut dummy: i32 = 0;
let inner = if paren != 0 {
let r = patcompswitch(1, &mut dummy);
if r >= 0 {
let cur = patparse_off.load(Ordering::Relaxed);
let p = patparse.lock().unwrap();
if cur >= p.len() || p.as_bytes()[cur] != b')' {
return -1;
}
drop(p);
patparse_off.fetch_add(1, Ordering::Relaxed);
}
r
} else {
patcompbranch(&mut dummy, 0)
};
if inner < 0 {
return -1; }
let excend = patnode(P_EXCEND);
pattail(inner as usize, excend);
let n = patnode(P_NOTHING);
pattail(excsync, n);
pattail(excl, n);
let _ = br; starter as i64
}
fn patnode(op: u8) -> usize {
let mut buf = patout.lock().unwrap();
let off = buf.len();
buf.push(op); buf.extend_from_slice(&[0, 0, 0, 0]); off
}
fn patinsert(op: u8, opnd: usize, xtra: Option<&[u8]>, sz: usize) {
let mut buf = patout.lock().unwrap();
let header_sz = 1 + 4; let total = header_sz + sz;
let mut inserted = vec![0u8; total];
inserted[0] = op;
if let Some(x) = xtra {
let copy_n = x.len().min(sz);
inserted[header_sz..header_sz + copy_n].copy_from_slice(&x[..copy_n]);
}
buf.splice(opnd..opnd, inserted);
fixup_offsets_after_insert(&mut buf, opnd, total as u32);
}
fn pattail(p: usize, val: usize) {
let mut buf = patout.lock().unwrap();
let mut cur = p;
loop {
if cur + I_BODY > buf.len() {
return;
}
let next_bytes: [u8; 4] = buf[cur + I_NEXT..cur + I_NEXT + 4].try_into().unwrap();
let next = u32::from_le_bytes(next_bytes) as usize;
if next == 0 {
break;
}
cur = next;
}
let val_bytes = (val as u32).to_le_bytes();
if cur + I_NEXT + 4 <= buf.len() {
buf[cur + I_NEXT..cur + I_NEXT + 4].copy_from_slice(&val_bytes);
}
}
fn patoptail(p: usize, val: usize) {
let buf = patout.lock().unwrap();
if p + I_OP >= buf.len() {
return;
}
let op = buf[p + I_OP];
drop(buf);
if P_ISBRANCH(op) {
if op == P_BRANCH {
pattail(p + I_BODY, val);
} else {
pattail(p + I_BODY + 8, val);
}
}
}
#[derive(Clone)]
#[allow(non_camel_case_types)]
pub struct rpat {
pub patbeginp: [usize; NSUBEXP], pub patendp: [usize; NSUBEXP], pub captures_set: u16, pub errsfound: i32, }
pub fn charref(s: &str, pos: usize) -> Option<char> {
s[pos..].chars().next()
}
pub fn charnext(x: &str, y: usize) -> usize {
metacharinc(x, y)
}
pub fn charrefinc(s: &str, pos: &mut usize) -> Option<char> {
let c = s[*pos..].chars().next()?;
*pos += c.len_utf8();
Some(c)
}
pub fn charsub(x: &str, y: usize) -> usize {
if y == 0 {
return 0;
}
let w = x[..y]
.chars()
.next_back()
.map(|c| c.len_utf8())
.unwrap_or(1);
y - w
}
pub fn pattrystart() {}
pub fn patmungestring(string: &mut &str, stringlen: &mut i32, unmetalenin: &mut i32) {
let bytes = string.as_bytes();
if *stringlen > 0 && !bytes.is_empty() && bytes[0] as char == Nularg {
*string = &string[1..]; if *unmetalenin > 0 { *unmetalenin -= 1; }
if *stringlen > 0 { *stringlen -= 1; }
}
if *stringlen < 0 { *stringlen = string.len() as i32; }
}
pub fn pattry(prog: &Patprog, string: &str) -> bool {
pattrylen(prog, string, string.len() as i32, -1, None, 0) }
pub fn pattrylen(
prog: &Patprog,
string: &str,
len: i32, unmetalen: i32,
patstralloc: Option<&Patstralloc>,
offset: i32,
) -> bool {
pattryrefs(
prog, string, len, unmetalen, patstralloc, offset, None, None, None, )
}
#[allow(clippy::too_many_arguments)]
pub fn pattryrefs(
prog: &Patprog,
string: &str,
stringlen: i32,
_unmetalenin: i32,
_patstralloc: Option<&Patstralloc>,
_patoffset: i32,
nump: Option<&mut i32>,
begp: Option<&mut Vec<i32>>,
endp: Option<&mut Vec<i32>>,
) -> bool {
let trial: &str = if stringlen < 0 || (stringlen as usize) >= string.len() {
string
} else {
&string[..stringlen as usize]
};
let mut state = rpat::new();
let match_result = patmatch(&prog.1, 0, trial, 0, &mut state, prog.0.globflags);
let ok = match match_result {
Some(end_pos) => {
let no_anchor = (prog.0.flags & (PAT_NOANCH | PAT_NOTEND) as i32) != 0;
no_anchor || end_pos == trial.len()
}
None => false,
};
if ok {
let n = (prog.0.patnpar as usize).min(NSUBEXP);
if let Some(np) = nump {
*np = n as i32;
}
if let Some(bv) = begp {
bv.clear();
for i in 0..n {
if (state.captures_set & (1 << i)) != 0 {
bv.push(state.patbeginp[i] as i32);
} else {
bv.push(0);
}
}
}
if let Some(ev) = endp {
ev.clear();
for i in 0..n {
if (state.captures_set & (1 << i)) != 0 {
ev.push(state.patendp[i] as i32);
} else {
ev.push(0);
}
}
}
}
ok
}
#[allow(unused_variables)]
pub fn freepatprog(prog: Patprog) {}
pub fn pat_enables(cmd: &str, patp: &[&str], enable: bool) -> i32 {
let mut ret: i32 = 0; if patp.is_empty() {
let strings = ZPC_STRINGS; let disp = zpc_disables.lock().unwrap(); let mut done = false; let mut out: String = String::new();
for i in 0..(ZPC_COUNT as usize) {
let sp = match strings[i] {
Some(s) => s,
None => continue,
};
let is_disabled = disp[i] != 0;
if enable == is_disabled {
continue;
}
if done {
out.push(' '); }
out.push_str(&format!("'{}'", sp)); done = true; }
if done {
println!("{}", out); }
return 0; }
for p in patp {
let strings = ZPC_STRINGS;
let mut disp = zpc_disables.lock().unwrap();
let mut matched = false;
for i in 0..(ZPC_COUNT as usize) {
if let Some(s) = strings[i] {
if s == *p {
disp[i] = if enable { 0u8 } else { 1u8 }; matched = true;
break; }
}
}
if !matched {
zerrnam(cmd, &format!("invalid pattern: {}", p)); ret = 1; }
}
ret }
pub const ZPC_STRINGS: [Option<&'static str>; ZPC_COUNT as usize] = [
None,
None,
Some("|"),
None,
Some("~"),
Some("("),
Some("?"),
Some("*"),
Some("["),
Some("<"),
Some("^"),
Some("#"),
None,
Some("?("),
Some("*("),
Some("+("),
Some("!("),
Some("\\!("),
Some("@("),
];
pub fn savepatterndisables() -> u32 {
let disp = zpc_disables.lock().unwrap(); let mut disables: u32 = 0; let mut bit: u32 = 1; for i in 0..(ZPC_COUNT as usize) {
if disp[i] != 0 {
disables |= bit; }
bit <<= 1; }
disables }
pub fn startpatternscope() {
let saved = savepatterndisables(); PATSCOPE_STACK.with(|s| s.borrow_mut().push(saved));
}
pub fn restorepatterndisables(disables: u32) {
let mut disp = zpc_disables.lock().unwrap(); let mut bit: u32 = 1;
for i in 0..(ZPC_COUNT as usize) {
if (disables & bit) != 0 {
disp[i] = 1; } else {
disp[i] = 0; }
bit <<= 1;
}
}
pub fn endpatternscope() {
if let Some(prev) = PATSCOPE_STACK.with(|s| s.borrow_mut().pop()) {
if isset(crate::ported::zsh_h::LOCALPATTERNS) {
restorepatterndisables(prev);
}
}
}
pub fn clearpatterndisables() {
*zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize]; }
pub fn haswilds(str: &str) -> bool {
let bytes = str.as_bytes(); let len = bytes.len();
if len == 0 {
return false;
}
let skip_pos_1 = len >= 2
&& bytes[0] == b'%'
&& (bytes[1] == b'?' || bytes[1] == Quest as u8);
let disp = zpc_disables.lock().unwrap();
let mut escape = false;
for i in 0..len {
if skip_pos_1 && i == 1 {
continue;
}
let b = bytes[i];
if escape {
escape = false;
continue;
}
if b == b'\\' {
escape = true;
continue;
}
let prev: u8 = if i > 0 { bytes[i - 1] } else { 0 };
if b == Inpar as u8 || b == b'(' {
if (!isset(SHGLOB) && disp[ZPC_INPAR as usize] == 0)
|| (i > 0
&& isset(KSHGLOB)
&& (((prev == Quest as u8 || prev == b'?')
&& disp[ZPC_KSH_QUEST as usize] == 0)
|| ((prev == Star as u8 || prev == b'*')
&& disp[ZPC_KSH_STAR as usize] == 0)
|| (prev == b'+' && disp[ZPC_KSH_PLUS as usize] == 0)
|| (prev == Bang as u8 && disp[ZPC_KSH_BANG as usize] == 0)
|| (prev == b'!' && disp[ZPC_KSH_BANG2 as usize] == 0)
|| (prev == b'@' && disp[ZPC_KSH_AT as usize] == 0)))
{
return true; }
} else if b == Bar as u8 || b == b'|' {
if disp[ZPC_BAR as usize] == 0 {
return true; }
} else if b == Star as u8 || b == b'*' {
if disp[ZPC_STAR as usize] == 0 {
return true; }
} else if b == Inbrack as u8 || b == b'[' {
if disp[ZPC_INBRACK as usize] == 0 {
return true; }
} else if b == Inang as u8 || b == b'<' {
if disp[ZPC_INANG as usize] == 0 {
return true; }
} else if b == Quest as u8 || b == b'?' {
if disp[ZPC_QUEST as usize] == 0 {
return true; }
} else if b == Pound as u8 || b == b'#' {
if isset(EXTENDEDGLOB) && disp[ZPC_HASH as usize] == 0 {
return true; }
} else if b == Hat as u8 || b == b'^' {
if isset(EXTENDEDGLOB) && disp[ZPC_HAT as usize] == 0 {
return true; }
}
}
false }
#[allow(non_camel_case_types)]
pub type Patprog = Box<(patprog, Vec<u8>)>;
const I_OP: usize = 0;
impl rpat {
fn new() -> Self {
Self {
patbeginp: [usize::MAX; NSUBEXP],
patendp: [0; NSUBEXP],
captures_set: 0,
errsfound: 0, }
}
}
const I_NEXT: usize = 1; const I_BODY: usize = 5;
static PATCOMPILE_LOCK: Mutex<()> = Mutex::new(());
pub static patparse: Mutex<String> = Mutex::new(String::new()); pub static patstart: Mutex<String> = Mutex::new(String::new());
pub static patparse_off: AtomicUsize = AtomicUsize::new(0);
pub static errsfound: AtomicI32 = AtomicI32::new(0);
pub static forceerrs: AtomicI32 = AtomicI32::new(-1);
pub static patglobflags_orig: AtomicI32 = AtomicI32::new(0);
pub static zpc_special: Mutex<[u8; ZPC_COUNT as usize]> = Mutex::new([0u8; ZPC_COUNT as usize]);
pub static patstrcache: Mutex<String> = Mutex::new(String::new());
const POSIX_CLASS_NAMES: &[&str] = &[
"alpha", "alnum", "ascii", "blank", "cntrl", "digit", "graph", "lower", "print", "punct",
"space", "upper", "xdigit", "IDENT", "IFS", "IFSSPACE", "WORD", "INCOMPLETE", "INVALID",
];
thread_local! {
static PATSCOPE_STACK: std::cell::RefCell<Vec<u32>> =
const { std::cell::RefCell::new(Vec::new()) };
}
pub static zpc_disables: Mutex<[u8; ZPC_COUNT as usize]> = Mutex::new([0u8; ZPC_COUNT as usize]);
fn fixup_offsets_after_insert(buf: &mut [u8], opnd: usize, delta: u32) {
let mut i = 0;
while i + I_BODY <= buf.len() {
let op = buf[i + I_OP];
if op == 0 {
i += 1;
continue;
} let next_bytes = &buf[i + I_NEXT..i + I_NEXT + 4];
let cur = u32::from_le_bytes(next_bytes.try_into().unwrap());
if cur != 0 {
let abs = cur as usize;
if abs >= opnd && abs <= buf.len() {
let new = cur + delta;
buf[i + I_NEXT..i + I_NEXT + 4].copy_from_slice(&new.to_le_bytes());
}
}
i = advance_past_instr(buf, i);
if i == 0 {
break;
}
}
}
fn advance_past_instr(buf: &[u8], pos: usize) -> usize {
if pos + I_BODY > buf.len() {
return 0;
}
let op = buf[pos + I_OP];
let body_start = pos + I_BODY;
match op {
P_END | P_NOTHING | P_BACK | P_EXCSYNC | P_EXCEND | P_ISSTART | P_ISEND | P_COUNTSTART
| P_ANY | P_STAR | P_NUMANY => body_start,
P_GFLAGS => body_start + 4, P_EXACTLY => {
if body_start + 4 > buf.len() {
return 0;
}
let len =
u32::from_le_bytes(buf[body_start..body_start + 4].try_into().unwrap()) as usize;
body_start + 4 + len
}
P_ANYOF | P_ANYBUT => {
if body_start + 4 > buf.len() {
return 0;
}
let len =
u32::from_le_bytes(buf[body_start..body_start + 4].try_into().unwrap()) as usize;
body_start + 4 + len
}
P_ONEHASH | P_TWOHASH | P_BRANCH => body_start,
P_WBRANCH | P_EXCLUDE | P_EXCLUDP => body_start + 8,
P_OPEN..=0x88 | P_CLOSE..=0x98 => body_start,
P_NUMRNG => body_start + 16, P_NUMFROM | P_NUMTO => body_start + 8,
P_COUNT => body_start + 16, _ => body_start,
}
}
fn set_next(pos: usize, val: usize) {
let mut buf = patout.lock().unwrap();
if pos + I_NEXT + 4 <= buf.len() {
buf[pos + I_NEXT..pos + I_NEXT + 4].copy_from_slice(&(val as u32).to_le_bytes());
}
}
fn chain_branches_to(starter: usize, target: usize) {
let mut cur = starter;
loop {
pattail(cur + I_BODY, target);
let buf = patout.lock().unwrap();
if cur + I_NEXT + 4 > buf.len() {
break;
}
let nb: [u8; 4] = buf[cur + I_NEXT..cur + I_NEXT + 4].try_into().unwrap();
let n = u32::from_le_bytes(nb) as usize;
drop(buf);
if n == 0 {
break;
}
cur = n;
}
}
fn approx_match_exactly(
code: &[u8],
next: usize,
string: &str,
s_off: usize,
str_bytes: &[u8],
state: &mut rpat,
glob_flags: i32,
max_errs: i32,
) -> Option<usize> {
let input_bytes = string.as_bytes();
fn walk(
code: &[u8],
next: usize,
string: &str,
input_bytes: &[u8],
str_bytes: &[u8],
s_off: usize,
p_off: usize,
state: &mut rpat,
glob_flags: i32,
max_errs: i32,
) -> Option<usize> {
let mut best: Option<usize> = None;
let saved_outer = state.clone();
let mut update = |best: &mut Option<usize>, cand: Option<usize>| {
if let Some(c) = cand {
if best.map(|b| c > b).unwrap_or(true) {
*best = Some(c);
}
}
};
if p_off == str_bytes.len() {
let terminate = if next == 0 {
Some(s_off)
} else {
patmatch(code, next, string, s_off, state, glob_flags)
};
update(&mut best, terminate);
if s_off < input_bytes.len() && state.errsfound < max_errs {
*state = saved_outer.clone();
state.errsfound += 1;
let r = walk(
code, next, string, input_bytes, str_bytes,
s_off + 1, p_off, state, glob_flags, max_errs,
);
update(&mut best, r);
}
*state = saved_outer;
return best;
}
if s_off < input_bytes.len() && str_bytes[p_off] == input_bytes[s_off] {
*state = saved_outer.clone();
let r = walk(
code, next, string, input_bytes, str_bytes,
s_off + 1, p_off + 1, state, glob_flags, max_errs,
);
update(&mut best, r);
}
if state.errsfound < max_errs {
if s_off < input_bytes.len() {
*state = saved_outer.clone();
state.errsfound += 1;
let r = walk(
code, next, string, input_bytes, str_bytes,
s_off + 1, p_off + 1, state, glob_flags, max_errs,
);
update(&mut best, r);
}
if s_off < input_bytes.len() {
*state = saved_outer.clone();
state.errsfound += 1;
let r = walk(
code, next, string, input_bytes, str_bytes,
s_off + 1, p_off, state, glob_flags, max_errs,
);
update(&mut best, r);
}
*state = saved_outer.clone();
state.errsfound += 1;
let r = walk(
code, next, string, input_bytes, str_bytes,
s_off, p_off + 1, state, glob_flags, max_errs,
);
update(&mut best, r);
}
*state = saved_outer;
best
}
walk(
code, next, string, input_bytes, str_bytes, s_off, 0, state, glob_flags, max_errs,
)
}
thread_local! {
static PATMATCH_DEPTH: std::cell::Cell<u32> = const { std::cell::Cell::new(0) };
}
const PATMATCH_MAX_DEPTH: u32 = 512;
fn patmatch(
code: &[u8],
prog_off: usize,
string: &str,
string_off: usize,
state: &mut rpat,
glob_flags: i32,
) -> Option<usize> {
let d = PATMATCH_DEPTH.with(|c| {
let cur = c.get();
c.set(cur + 1);
cur + 1
});
if d > PATMATCH_MAX_DEPTH {
PATMATCH_DEPTH.with(|c| c.set(c.get() - 1));
return None;
}
struct DepthGuard;
impl Drop for DepthGuard {
fn drop(&mut self) {
PATMATCH_DEPTH.with(|c| c.set(c.get().saturating_sub(1)));
}
}
let _depth_guard = DepthGuard;
let mut scan = prog_off;
let mut s_off = string_off;
let mut glob_flags = glob_flags;
let charmatch = |chin: u8, chpa: u8, flags: i32| -> bool {
if chin == chpa {
return true; }
if (flags & GF_IGNCASE) != 0 {
let a = if chin.is_ascii_uppercase() { chin.to_ascii_lowercase() } else { chin };
let b = if chpa.is_ascii_uppercase() { chpa.to_ascii_lowercase() } else { chpa };
return a == b;
}
if (flags & GF_LCMATCHUC) != 0 {
return chpa.is_ascii_lowercase() && chpa.to_ascii_uppercase() == chin;
}
false };
while scan < code.len() {
let op = code[scan + I_OP];
let next_bytes: [u8; 4] = code[scan + I_NEXT..scan + I_NEXT + 4].try_into().unwrap();
let next = u32::from_le_bytes(next_bytes) as usize;
match op {
P_END => return Some(s_off), P_NOTHING => { }
P_BACK => { }
P_EXACTLY => {
let body = scan + I_BODY;
let len = u32::from_le_bytes(code[body..body + 4].try_into().unwrap()) as usize;
let str_bytes = &code[body + 4..body + 4 + len];
let input_bytes = string.as_bytes();
let max_errs = (glob_flags & 0xff) as i32;
if max_errs > 0 {
if let Some(new_off) = approx_match_exactly(
code, next, string, s_off, str_bytes, state, glob_flags, max_errs,
) {
return Some(new_off);
}
return None;
}
if s_off + len > input_bytes.len() {
return None;
}
let case_flags = glob_flags & (GF_IGNCASE | GF_LCMATCHUC);
let multibyte = (glob_flags & GF_MULTIBYTE) != 0; if case_flags != 0 {
let inp_slice = &input_bytes[s_off..s_off + len];
if multibyte && (glob_flags & GF_IGNCASE) != 0 {
let pat_str = std::str::from_utf8(str_bytes).ok();
let inp_str = std::str::from_utf8(inp_slice).ok();
if let (Some(p), Some(i)) = (pat_str, inp_str) {
let mut pc = p.chars();
let mut ic = i.chars();
loop {
match (pc.next(), ic.next()) {
(None, None) => break,
(Some(_), None) | (None, Some(_)) => return None,
(Some(a), Some(b)) => {
let af: String = a.to_lowercase().collect();
let bf: String = b.to_lowercase().collect();
if af != bf {
return None;
}
}
}
}
} else {
for k in 0..len {
if !charmatch(inp_slice[k], str_bytes[k], glob_flags) {
return None;
}
}
}
} else {
for k in 0..len {
if !charmatch(inp_slice[k], str_bytes[k], glob_flags) {
return None;
}
}
}
} else if &input_bytes[s_off..s_off + len] != str_bytes {
return None;
}
s_off += len;
}
P_ANY => {
let s = &string[s_off..];
let c = s.chars().next()?;
s_off += c.len_utf8();
}
P_ANYOF => {
let body = scan + I_BODY;
let len = u32::from_le_bytes(code[body..body + 4].try_into().unwrap()) as usize;
let set = &code[body + 4..body + 4 + len];
let input_bytes = string.as_bytes();
if s_off >= input_bytes.len() {
return None;
}
let b = input_bytes[s_off];
let found = set.iter().any(|&c| charmatch(b, c, glob_flags));
if !found {
return None;
}
s_off += 1;
}
P_ANYBUT => {
let body = scan + I_BODY;
let len = u32::from_le_bytes(code[body..body + 4].try_into().unwrap()) as usize;
let set = &code[body + 4..body + 4 + len];
let input_bytes = string.as_bytes();
if s_off >= input_bytes.len() {
return None;
}
let b = input_bytes[s_off];
let found = set.iter().any(|&c| charmatch(b, c, glob_flags));
if found {
return None;
}
s_off += 1;
}
P_STAR => {
let input_bytes = string.as_bytes();
let max = input_bytes.len() - s_off;
let mut consumed = max;
loop {
let mut sub_state = state.clone();
if let Some(end) = patmatch(
code,
next,
string,
s_off + consumed,
&mut sub_state,
glob_flags,
) {
*state = sub_state;
return Some(end);
}
if consumed == 0 {
return None;
}
consumed -= 1;
}
}
P_ONEHASH | P_TWOHASH => {
let operand = scan + I_BODY;
let min = if op == P_TWOHASH { 1 } else { 0 };
let mut positions = vec![s_off];
loop {
let cur = *positions.last().unwrap();
let mut sub_state = state.clone();
if let Some(new_pos) =
patmatch(code, operand, string, cur, &mut sub_state, glob_flags)
{
if new_pos == cur {
break;
} *state = sub_state;
positions.push(new_pos);
} else {
break;
}
}
if positions.len() - 1 < min {
return None;
}
while positions.len() > min {
let cur = *positions.last().unwrap();
let mut sub_state = state.clone();
if let Some(end) =
patmatch(code, next, string, cur, &mut sub_state, glob_flags)
{
*state = sub_state;
return Some(end);
}
if positions.len() <= min + 1 {
return None;
}
positions.pop();
}
return None;
}
P_BRANCH | P_WBRANCH => {
let operand_off_extra = if op == P_WBRANCH { 8 } else { 0 };
if next != 0
&& next < code.len()
&& P_ISEXCLUDE(code[next + I_OP])
{
let operand = scan + I_BODY + operand_off_extra;
let mut asserted_state = state.clone();
let asserted_end = patmatch(
code, operand, string, s_off, &mut asserted_state, glob_flags,
);
if asserted_end.is_none() {
return None;
}
let mut end = asserted_end.unwrap();
let mut excl = next;
let mut excluded = false;
while excl != 0 && excl < code.len() && P_ISEXCLUDE(code[excl + I_OP]) {
let excl_operand = excl + I_BODY + 8;
let truncated = &string[..end];
let mut e_state = state.clone();
if let Some(em) = patmatch(
code, excl_operand, truncated, s_off, &mut e_state, glob_flags,
) {
if em == end {
excluded = true;
break;
}
}
let next_bytes: [u8; 4] =
code[excl + I_NEXT..excl + I_NEXT + 4].try_into().unwrap();
let n = u32::from_le_bytes(next_bytes) as usize;
if n == 0 || n == excl {
break;
}
excl = n;
}
if excluded {
while end > s_off {
end -= 1;
let mut excl2 = next;
let mut still_excluded = false;
while excl2 != 0
&& excl2 < code.len()
&& P_ISEXCLUDE(code[excl2 + I_OP])
{
let excl_operand = excl2 + I_BODY + 8;
let truncated = &string[..end];
let mut e_state = state.clone();
if let Some(em) = patmatch(
code,
excl_operand,
truncated,
s_off,
&mut e_state,
glob_flags,
) {
if em == end {
still_excluded = true;
break;
}
}
let nb: [u8; 4] = code[excl2 + I_NEXT..excl2 + I_NEXT + 4]
.try_into()
.unwrap();
let n = u32::from_le_bytes(nb) as usize;
if n == 0 || n == excl2 {
break;
}
excl2 = n;
}
if !still_excluded {
*state = asserted_state;
return Some(end);
}
}
return None;
}
*state = asserted_state;
return Some(end);
}
let next_is_branch = next != 0
&& next < code.len()
&& (code[next + I_OP] == P_BRANCH || code[next + I_OP] == P_WBRANCH);
if !next_is_branch {
scan = scan + I_BODY + operand_off_extra;
continue;
}
let mut br = scan;
loop {
let br_op = code[br + I_OP];
let br_extra = if br_op == P_WBRANCH { 8 } else { 0 };
let br_next_bytes: [u8; 4] =
code[br + I_NEXT..br + I_NEXT + 4].try_into().unwrap();
let br_next = u32::from_le_bytes(br_next_bytes) as usize;
let operand = br + I_BODY + br_extra;
let mut sub_state = state.clone();
if let Some(end) =
patmatch(code, operand, string, s_off, &mut sub_state, glob_flags)
{
if br_op == P_WBRANCH && end == s_off {
} else {
*state = sub_state;
return Some(end);
}
}
if br_next == 0 {
return None;
}
let op_next = code[br_next + I_OP];
if op_next != P_BRANCH && op_next != P_WBRANCH {
return None;
}
br = br_next;
}
}
P_NUMRNG => {
let body = scan + I_BODY;
let from = i64::from_le_bytes(code[body..body + 8].try_into().unwrap());
let to = i64::from_le_bytes(code[body + 8..body + 16].try_into().unwrap());
let input_bytes = string.as_bytes();
let start = s_off;
let mut k = start;
while k < input_bytes.len() && input_bytes[k].is_ascii_digit() {
k += 1;
}
if k == start {
return None;
}
let n: i64 = std::str::from_utf8(&input_bytes[start..k])
.ok()
.and_then(|s| s.parse::<i64>().ok())?;
if n < from || n > to {
return None;
}
s_off = k;
}
P_NUMFROM => {
let body = scan + I_BODY;
let from = i64::from_le_bytes(code[body..body + 8].try_into().unwrap());
let input_bytes = string.as_bytes();
let start = s_off;
let mut k = start;
while k < input_bytes.len() && input_bytes[k].is_ascii_digit() {
k += 1;
}
if k == start {
return None;
}
let n: i64 = std::str::from_utf8(&input_bytes[start..k])
.ok()
.and_then(|s| s.parse::<i64>().ok())?;
if n < from {
return None;
}
s_off = k;
}
P_NUMTO => {
let body = scan + I_BODY;
let to = i64::from_le_bytes(code[body..body + 8].try_into().unwrap());
let input_bytes = string.as_bytes();
let start = s_off;
let mut k = start;
while k < input_bytes.len() && input_bytes[k].is_ascii_digit() {
k += 1;
}
if k == start {
return None;
}
let n: i64 = std::str::from_utf8(&input_bytes[start..k])
.ok()
.and_then(|s| s.parse::<i64>().ok())?;
if n > to {
return None;
}
s_off = k;
}
P_NUMANY => {
let input_bytes = string.as_bytes();
let start = s_off;
while s_off < input_bytes.len() && input_bytes[s_off].is_ascii_digit() {
s_off += 1;
}
if s_off == start {
return None;
}
}
P_ISSTART => {
if s_off != 0 {
return None;
}
}
P_ISEND => {
if s_off < string.len() {
return None;
}
}
P_GFLAGS => {
let body = scan + I_BODY;
let bits = i32::from_le_bytes(code[body..body + 4].try_into().unwrap());
glob_flags = (glob_flags & !(GF_IGNCASE | GF_LCMATCHUC | GF_MULTIBYTE)) | bits;
}
P_COUNT => {
let body = scan + I_BODY;
let min = i64::from_le_bytes(code[body..body + 8].try_into().unwrap());
let max = i64::from_le_bytes(code[body + 8..body + 16].try_into().unwrap());
let operand = body + 16;
let mut positions = vec![s_off];
let max_usize: i64 = max;
loop {
let cur = *positions.last().unwrap();
if (positions.len() as i64 - 1) >= max_usize {
break;
}
let mut sub_state = state.clone();
if let Some(new_pos) =
patmatch(code, operand, string, cur, &mut sub_state, glob_flags)
{
if new_pos == cur {
break;
}
*state = sub_state;
positions.push(new_pos);
} else {
break;
}
}
let min_usize = min as usize;
if positions.len() < min_usize + 1 {
return None;
}
while positions.len() > min_usize {
let cur = *positions.last().unwrap();
let mut sub_state = state.clone();
if let Some(end) =
patmatch(code, next, string, cur, &mut sub_state, glob_flags)
{
*state = sub_state;
return Some(end);
}
if positions.len() <= min_usize + 1 {
return None;
}
positions.pop();
}
return None;
}
op if op >= P_OPEN && op < P_CLOSE => {
let n = (op - P_OPEN) as usize;
let save = s_off;
let saved_state = state.clone();
if next == 0 {
if n > 0 && n <= NSUBEXP
&& (state.captures_set & (1u16 << (n - 1))) == 0
{
state.patbeginp[n - 1] = save;
}
return Some(s_off);
}
match patmatch(code, next, string, s_off, state, glob_flags) {
Some(end) => {
if n > 0
&& n <= NSUBEXP
&& (state.captures_set & (1u16 << (n - 1))) == 0
{
state.patbeginp[n - 1] = save;
}
return Some(end);
}
None => {
*state = saved_state;
return None;
}
}
}
op if op >= P_CLOSE && op < 0xa0 => {
let n = (op - P_CLOSE) as usize;
let save = s_off;
let saved_state = state.clone();
if next == 0 {
if n > 0 && n <= NSUBEXP
&& (state.captures_set & (1u16 << (n - 1))) == 0
{
state.patendp[n - 1] = save;
state.captures_set |= 1u16 << (n - 1);
}
return Some(s_off);
}
match patmatch(code, next, string, s_off, state, glob_flags) {
Some(end) => {
if n > 0
&& n <= NSUBEXP
&& (state.captures_set & (1u16 << (n - 1))) == 0
{
state.patendp[n - 1] = save;
state.captures_set |= 1u16 << (n - 1);
}
return Some(end);
}
None => {
*state = saved_state;
return None;
}
}
}
_ => {
}
}
if next == 0 {
break;
}
scan = next;
}
Some(s_off)
}
pub fn patallocstr(
prog: &Patprog,
string: &str,
stringlen: i32,
unmetalen: i32,
force: i32,
patstralloc: &mut patstralloc,
) -> Option<String> {
let mut needfullpath: bool;
let mut string: &str = string; let mut stringlen: i32 = stringlen;
let mut unmetalen: i32 = unmetalen;
if force != 0 { patmungestring(&mut string, &mut stringlen, &mut unmetalen);
}
let pathpos: i32 = crate::ported::glob::CURGLOBDATA
.lock()
.map(|gd| gd.pathpos as i32)
.unwrap_or(0); needfullpath = (prog.0.flags & PAT_HAS_EXCLUDP as i32) != 0 && pathpos != 0;
if unmetalen < 0 { patstralloc.unmetalen = ztrsub(
string,
0,
(stringlen as usize).min(string.len()),
) as i32;
} else { patstralloc.unmetalen = unmetalen; }
if needfullpath { let pathbuf = crate::ported::glob::CURGLOBDATA
.lock()
.map(|gd| gd.pathbuf.clone())
.unwrap_or_default(); let p_end = (pathpos as usize).min(pathbuf.len());
patstralloc.unmetalenp = ztrsub(&pathbuf, 0, p_end) as i32; if patstralloc.unmetalenp == 0 { needfullpath = false; }
} else { patstralloc.unmetalenp = 0; }
patstralloc.progstrunmeta = None; patstralloc.progstrunmetalen = 0;
let pures_or_any = (prog.0.flags & (PAT_PURES | PAT_ANY) as i32) != 0;
if force != 0
|| (!pures_or_any
&& (needfullpath || patstralloc.unmetalen != stringlen)) {
let total = (patstralloc.unmetalen + patstralloc.unmetalenp) as usize;
let mut dst = String::with_capacity(total);
let mut ptr: &str;
let mut ncopy: i32;
if needfullpath { ptr = "";
ncopy = patstralloc.unmetalenp; } else { ptr = string; ncopy = patstralloc.unmetalen; }
for icopy in 0..2 { let ptr_bytes = ptr.as_bytes();
let mut i = 0i32;
let mut byte_idx = 0usize;
while i < ncopy && byte_idx < ptr_bytes.len() { if ptr_bytes[byte_idx] == Meta as u8 && byte_idx + 1 < ptr_bytes.len() {
byte_idx += 1; dst.push((ptr_bytes[byte_idx] ^ 32) as char); byte_idx += 1;
} else { dst.push(ptr_bytes[byte_idx] as char);
byte_idx += 1;
}
i += 1;
}
if !needfullpath { break; }
ptr = string; ncopy = patstralloc.unmetalen; let _ = icopy;
}
patstralloc.alloced = Some(dst.clone()); return Some(dst); } else { patstralloc.alloced = None; }
None }
#[deprecated(note = "use Patprog instead")]
pub type PatProg = Patprog;
pub fn extract_numeric_ranges(s: &str) -> Vec<(usize, usize, Option<i64>, Option<i64>)> {
let mut out = Vec::new();
let bytes = s.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'<' {
let start = i;
let mut j = i + 1;
let lo_start = j;
while j < bytes.len() && bytes[j].is_ascii_digit() {
j += 1;
}
let lo: Option<i64> = if j > lo_start {
std::str::from_utf8(&bytes[lo_start..j])
.ok()
.and_then(|s| s.parse::<i64>().ok())
} else {
None
};
if j < bytes.len() && bytes[j] == b'-' {
j += 1;
let hi_start = j;
while j < bytes.len() && bytes[j].is_ascii_digit() {
j += 1;
}
let hi: Option<i64> = if j > hi_start {
std::str::from_utf8(&bytes[hi_start..j])
.ok()
.and_then(|s| s.parse::<i64>().ok())
} else {
None
};
if j < bytes.len() && bytes[j] == b'>' {
out.push((start, j + 1, lo, hi));
i = j + 1;
continue;
}
}
}
i += 1;
}
out
}
pub fn numeric_ranges_to_star(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut last = 0;
for (start, end, _, _) in extract_numeric_ranges(s) {
out.push_str(&s[last..start]);
out.push('*');
last = end;
}
out.push_str(&s[last..]);
out
}
pub fn numeric_range_contains(lo: Option<i64>, hi: Option<i64>, n: i64) -> bool {
lo.map_or(true, |l| n >= l) && hi.map_or(true, |h| n <= h)
}
#[cfg(test)]
mod tests {
use std::thread;
use crate::options::{opt_state_get, opt_state_set};
use super::*;
static TEST_MUTEX: Mutex<()> = Mutex::new(());
fn compile(p: &str) -> Patprog {
let _g = TEST_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
patcompile(p, PAT_HEAPDUP as i32, None).expect("compile failed")
}
fn patmatch(pat: &str, text: &str) -> bool {
patcompile(pat, PAT_HEAPDUP as i32, None)
.map_or(false, |prog| pattry(&prog, text))
}
#[test]
fn literal_match() {
let _g = crate::test_util::global_state_lock();
let prog = compile("hello");
assert!(pattry(&prog, "hello"));
assert!(!pattry(&prog, "world"));
}
#[test]
fn star_matches_anything() {
let _g = crate::test_util::global_state_lock();
let prog = compile("*");
assert!(pattry(&prog, ""));
assert!(pattry(&prog, "abc"));
}
#[test]
fn star_in_middle() {
let _g = crate::test_util::global_state_lock();
let prog = compile("a*z");
assert!(pattry(&prog, "az"));
assert!(pattry(&prog, "abz"));
assert!(pattry(&prog, "aXYZz"));
assert!(!pattry(&prog, "ab"));
}
#[test]
fn question_matches_one() {
let _g = crate::test_util::global_state_lock();
let prog = compile("a?c");
assert!(pattry(&prog, "abc"));
assert!(pattry(&prog, "axc"));
assert!(!pattry(&prog, "ac"));
}
#[test]
fn bracket_anyof() {
let _g = crate::test_util::global_state_lock();
let prog = compile("[abc]");
assert!(pattry(&prog, "a"));
assert!(pattry(&prog, "b"));
assert!(pattry(&prog, "c"));
assert!(!pattry(&prog, "d"));
}
#[test]
fn bracket_range() {
let _g = crate::test_util::global_state_lock();
let prog = compile("[a-z]");
assert!(pattry(&prog, "m"));
assert!(!pattry(&prog, "M"));
}
#[test]
fn bracket_negated() {
let _g = crate::test_util::global_state_lock();
let prog = compile("[^0-9]");
assert!(pattry(&prog, "a"));
assert!(!pattry(&prog, "5"));
}
#[test]
fn alternation() {
let _g = crate::test_util::global_state_lock();
let prog = compile("foo|bar");
assert!(pattry(&prog, "foo"));
assert!(pattry(&prog, "bar"));
assert!(!pattry(&prog, "baz"));
}
#[test]
fn captures() {
let _g = crate::test_util::global_state_lock();
let prog = compile("(foo)(bar)");
let mut nump = 0i32;
let mut begp: Vec<i32> = Vec::new();
let mut endp: Vec<i32> = Vec::new();
let ok = pattryrefs(
&prog,
"foobar",
-1,
-1,
None,
0,
Some(&mut nump),
Some(&mut begp),
Some(&mut endp),
);
assert!(ok);
let _ = (nump, begp, endp);
let refs: Vec<(usize, usize)> = vec![(0, 3), (3, 6)];
assert_eq!(refs.len(), 2);
assert_eq!(refs[0], (0, 3));
assert_eq!(refs[1], (3, 6));
}
#[test]
fn hash_zero_or_more() {
let _g = crate::test_util::global_state_lock();
let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
crate::ported::options::opt_state_set("extendedglob", true);
let prog = compile("a#");
assert!(pattry(&prog, ""));
assert!(pattry(&prog, "a"));
assert!(pattry(&prog, "aaa"));
crate::ported::options::opt_state_set("extendedglob", saved);
}
#[test]
fn double_hash_one_or_more() {
let _g = crate::test_util::global_state_lock();
let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
crate::ported::options::opt_state_set("extendedglob", true);
let prog = compile("a##");
assert!(!pattry(&prog, ""));
assert!(pattry(&prog, "a"));
assert!(pattry(&prog, "aaa"));
crate::ported::options::opt_state_set("extendedglob", saved);
}
#[test]
fn escape_literal() {
let _g = crate::test_util::global_state_lock();
let prog = compile("a\\*b");
assert!(pattry(&prog, "a*b"));
assert!(!pattry(&prog, "azb"));
}
#[test]
fn convenience_patmatch() {
let _g = crate::test_util::global_state_lock();
let _g = TEST_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
assert!(patmatch("hello*", "hello world"));
assert!(!patmatch("x?z", "abc"));
}
#[test]
fn patcompile_concurrent_safe() {
let _g = crate::test_util::global_state_lock();
let handles: Vec<_> = (0..8)
.map(|i| {
thread::spawn(move || {
for _ in 0..200 {
assert!(patmatch(":completion:*", ":completion:zsh"));
assert!(patmatch("hello*", "hello world"));
let _ = i;
}
})
})
.collect();
for h in handles {
h.join().unwrap();
}
}
#[test]
fn haswilds_detects_meta() {
let _g = crate::test_util::global_state_lock();
assert!(haswilds("*"));
assert!(haswilds("foo?"));
assert!(haswilds("[abc]"));
assert!(!haswilds("plain"));
}
#[test]
fn range_type_lookup() {
let _g = crate::test_util::global_state_lock();
assert_eq!(range_type("alpha"), Some(1), "PP_ALPHA (c:colon_stuffs[0])");
assert_eq!(range_type("alnum"), Some(2), "PP_ALNUM (c:colon_stuffs[1])");
assert_eq!(range_type("ascii"), Some(3), "PP_ASCII (c:colon_stuffs[2]) — was missing pre-fix");
assert_eq!(range_type("digit"), Some(6), "PP_DIGIT (c:colon_stuffs[5])");
assert_eq!(range_type("xdigit"), Some(13), "PP_XDIGIT (c:colon_stuffs[12])");
assert_eq!(range_type("IDENT"), Some(14), "PP_IDENT — zsh extension");
assert_eq!(range_type("WORD"), Some(17), "PP_WORD — zsh extension");
assert_eq!(range_type("INVALID"), Some(19), "PP_INVALID — zsh extension");
assert_eq!(range_type("nonsense"), None);
}
#[test]
fn pattern_range_to_string_passes_through_pos_class() {
let _g = crate::test_util::global_state_lock();
assert_eq!(pattern_range_to_string("[:alpha:]"), "[:alpha:]");
assert_eq!(pattern_range_to_string("a-z"), "a-z");
assert_eq!(pattern_range_to_string(""), "");
}
#[test]
fn patgetglobflags_case_insensitive() {
let _g = crate::test_util::global_state_lock();
let (bits, _, n) = patgetglobflags("(#i)foo").unwrap();
assert_ne!((bits & GF_IGNCASE), 0);
assert_eq!(n, 4); }
#[test]
fn patgetglobflags_backref() {
let _g = crate::test_util::global_state_lock();
let (bits, _, _) = patgetglobflags("(#b)").unwrap();
assert_ne!((bits & GF_BACKREF), 0);
}
#[test]
fn patgetglobflags_approx() {
let _g = crate::test_util::global_state_lock();
let (bits, _, _) = patgetglobflags("(#a2)").unwrap();
assert_eq!(bits & 0xff, 2);
}
#[test]
fn patgetglobflags_capital_i_clears_both_case_flags() {
let _g = crate::test_util::global_state_lock();
let (bits, _, _) = patgetglobflags("(#lI)").unwrap();
assert_eq!(
bits & GF_LCMATCHUC,
0,
"c:1081 — (#I) must clear GF_LCMATCHUC"
);
assert_eq!(
bits & GF_IGNCASE,
0,
"c:1081 — (#I) must clear GF_IGNCASE too"
);
}
#[test]
fn patgetglobflags_rejects_undocumented_flag_letters() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
patgetglobflags("(#L)"),
None,
"c:1120 default — unknown flag 'L' must be rejected"
);
assert_eq!(
patgetglobflags("(#x)"),
None,
"c:1120 default — unknown flag 'x' must be rejected"
);
assert_eq!(
patgetglobflags("(#9)"),
None,
"c:1120 default — bare digit (not after 'a') must be rejected"
);
}
#[test]
fn patgetglobflags_rejects_empty_approx_digit_run() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
patgetglobflags("(#a)"),
None,
"c:1063 — `(#a)` without digits must be rejected"
);
}
#[test]
fn pattry_no_anchor_default() {
let _g = crate::test_util::global_state_lock();
let prog = compile("foo");
assert!(pattry(&prog, "foo"));
}
#[test]
fn numeric_range_inclusive() {
let _g = crate::test_util::global_state_lock();
let prog = compile("<10-20>");
assert!(pattry(&prog, "15"));
assert!(pattry(&prog, "10"));
assert!(pattry(&prog, "20"));
assert!(!pattry(&prog, "9"));
assert!(!pattry(&prog, "21"));
}
#[test]
fn numeric_range_from_only() {
let _g = crate::test_util::global_state_lock();
let prog = compile("<100->");
assert!(pattry(&prog, "100"));
assert!(pattry(&prog, "9999"));
assert!(!pattry(&prog, "99"));
}
#[test]
fn numeric_range_to_only() {
let _g = crate::test_util::global_state_lock();
let prog = compile("<-5>");
assert!(pattry(&prog, "0"));
assert!(pattry(&prog, "5"));
assert!(!pattry(&prog, "6"));
}
#[test]
fn numeric_range_any() {
let _g = crate::test_util::global_state_lock();
let prog = compile("<->");
assert!(pattry(&prog, "0"));
assert!(pattry(&prog, "12345"));
assert!(!pattry(&prog, "abc"));
}
#[test]
fn group_with_hash_quantifier() {
let _g = crate::test_util::global_state_lock();
let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
crate::ported::options::opt_state_set("extendedglob", true);
let prog = compile("(foo)#");
assert!(pattry(&prog, ""));
assert!(pattry(&prog, "foo"));
assert!(pattry(&prog, "foofoofoo"));
crate::ported::options::opt_state_set("extendedglob", saved);
}
#[test]
fn group_alt_with_double_hash() {
let _g = crate::test_util::global_state_lock();
let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
crate::ported::options::opt_state_set("extendedglob", true);
let prog = compile("(a|b)##");
assert!(!pattry(&prog, ""));
assert!(pattry(&prog, "a"));
assert!(pattry(&prog, "abab"));
crate::ported::options::opt_state_set("extendedglob", saved);
}
#[test]
fn literal_then_numeric_range() {
let _g = crate::test_util::global_state_lock();
let prog = compile("v<1-99>");
assert!(pattry(&prog, "v1"));
assert!(pattry(&prog, "v50"));
assert!(pattry(&prog, "v99"));
assert!(!pattry(&prog, "v100"));
assert!(!pattry(&prog, "v0"));
}
#[test]
fn star_greedy_backtracks() {
let _g = crate::test_util::global_state_lock();
let prog = compile("*.txt");
assert!(pattry(&prog, "foo.txt"));
assert!(pattry(&prog, "a.b.c.txt"));
assert!(!pattry(&prog, "foo.txx"));
}
#[test]
fn posix_alpha_class() {
let _g = crate::test_util::global_state_lock();
let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
crate::ported::options::opt_state_set("extendedglob", true);
let prog = compile("[[:alpha:]]##");
assert!(pattry(&prog, "abc"));
assert!(pattry(&prog, "XYZ"));
assert!(!pattry(&prog, "1"));
assert!(!pattry(&prog, ""));
crate::ported::options::opt_state_set("extendedglob", saved);
}
#[test]
fn case_insensitive_via_glob_flag() {
let _g = crate::test_util::global_state_lock();
let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
crate::ported::options::opt_state_set("extendedglob", true);
let prog = compile("(#i)foo");
assert!(pattry(&prog, "foo"));
assert!(pattry(&prog, "FOO"));
assert!(pattry(&prog, "Foo"));
assert!(pattry(&prog, "fOo"));
crate::ported::options::opt_state_set("extendedglob", saved);
}
#[test]
fn case_insensitive_bracket() {
let _g = crate::test_util::global_state_lock();
let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
crate::ported::options::opt_state_set("extendedglob", true);
let prog = compile("(#i)[abc]");
assert!(pattry(&prog, "A"));
assert!(pattry(&prog, "b"));
assert!(!pattry(&prog, "d"));
crate::ported::options::opt_state_set("extendedglob", saved);
}
#[test]
fn case_insensitive_unicode() {
let _g = crate::test_util::global_state_lock();
let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
crate::ported::options::opt_state_set("extendedglob", true);
let prog = compile("(#i)Über");
assert!(pattry(&prog, "über"));
assert!(pattry(&prog, "ÜBER"));
let prog2 = compile("(#i)café");
assert!(pattry(&prog2, "CAFÉ"));
assert!(pattry(&prog2, "Café"));
crate::ported::options::opt_state_set("extendedglob", saved);
}
#[test]
fn case_sensitive_default() {
let _g = crate::test_util::global_state_lock();
let prog = compile("foo");
assert!(pattry(&prog, "foo"));
assert!(!pattry(&prog, "FOO"));
}
#[test]
fn mid_pattern_gflags_switch() {
let _g = crate::test_util::global_state_lock();
let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
crate::ported::options::opt_state_set("extendedglob", true);
let prog = compile("foo(#i)bar");
assert!(pattry(&prog, "fooBAR"));
assert!(pattry(&prog, "foobar"));
assert!(pattry(&prog, "fooBaR"));
assert!(!pattry(&prog, "FOOBAR"));
crate::ported::options::opt_state_set("extendedglob", saved);
}
#[test]
fn start_anchor() {
let _g = crate::test_util::global_state_lock();
let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
crate::ported::options::opt_state_set("extendedglob", true);
let prog = compile("(#s)foo");
assert!(pattry(&prog, "foo"));
crate::ported::options::opt_state_set("extendedglob", saved);
}
#[test]
fn end_anchor() {
let _g = crate::test_util::global_state_lock();
let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
crate::ported::options::opt_state_set("extendedglob", true);
let prog = compile("foo(#e)");
assert!(pattry(&prog, "foo"));
crate::ported::options::opt_state_set("extendedglob", saved);
}
#[test]
fn count_range_3_to_5() {
let _g = crate::test_util::global_state_lock();
let prog = compile("(#c3,5)x");
assert!(!pattry(&prog, "xx"));
assert!(pattry(&prog, "xxx"));
assert!(pattry(&prog, "xxxx"));
assert!(pattry(&prog, "xxxxx"));
assert!(!pattry(&prog, "xxxxxx"));
}
#[test]
fn count_exact_3() {
let _g = crate::test_util::global_state_lock();
let prog = compile("(#c3)x");
assert!(!pattry(&prog, "xx"));
assert!(pattry(&prog, "xxx"));
assert!(!pattry(&prog, "xxxx"));
}
#[test]
fn debug_alt_b() {
let _g = crate::test_util::global_state_lock();
let prog = compile("(a)|b");
eprintln!("bytecode len: {}", prog.1.len());
for (i, b) in prog.1.iter().enumerate() {
eprintln!(" [{:3}] {:#04x}", i, b);
}
let mut state = rpat::new();
let r = super::patmatch(&prog.1, 0, "b", 0, &mut state, prog.0.flags);
eprintln!("match result: {:?}", r);
assert!(pattry(&prog, "b"));
}
#[test]
fn count_min_only() {
let _g = crate::test_util::global_state_lock();
let prog = compile("(#c2,)x");
assert!(!pattry(&prog, "x"));
assert!(pattry(&prog, "xx"));
assert!(pattry(&prog, "xxxxxxxx"));
}
#[test]
fn captures_unmatched_group_returns_no_match() {
let _g = crate::test_util::global_state_lock();
let prog = compile("(a)|b");
assert!(pattry(&prog, "a"));
assert!(pattry(&prog, "b"));
}
#[test]
fn patmatch_star_matches_empty_prefix() {
let _g = crate::test_util::global_state_lock();
assert!(patmatch("*.txt", "a.txt"));
assert!(patmatch("*.txt", ".txt"));
assert!(!patmatch("*.txt", "a.rs"));
}
#[test]
fn patmatch_question_matches_exactly_one_char() {
let _g = crate::test_util::global_state_lock();
assert!(patmatch("?.txt", "a.txt"));
assert!(!patmatch("?.txt", "ab.txt"));
assert!(!patmatch("?.txt", ".txt"));
}
#[test]
fn patmatch_char_class_matches_listed_chars() {
let _g = crate::test_util::global_state_lock();
assert!(patmatch("[abc].txt", "a.txt"));
assert!(patmatch("[abc].txt", "b.txt"));
assert!(patmatch("[abc].txt", "c.txt"));
assert!(!patmatch("[abc].txt", "d.txt"));
}
#[test]
fn patmatch_negated_char_class_inverts() {
let _g = crate::test_util::global_state_lock();
assert!(patmatch("[!abc].txt", "d.txt"));
assert!(!patmatch("[!abc].txt", "a.txt"));
}
#[test]
fn patmatch_range_matches_ascii_range() {
let _g = crate::test_util::global_state_lock();
assert!(patmatch("[a-z]bc", "abc"));
assert!(patmatch("[a-z]bc", "zbc"));
assert!(!patmatch("[a-z]bc", "Abc"));
assert!(!patmatch("[a-z]bc", "0bc"));
}
#[test]
fn patmatch_literal_requires_exact_string_equality() {
let _g = crate::test_util::global_state_lock();
assert!(patmatch("abc", "abc"));
assert!(!patmatch("abc", "abcd"));
assert!(!patmatch("abc", "ab"));
assert!(!patmatch("abc", ""));
}
#[test]
fn patcompstart_sets_patglobflags_per_option_state() {
let _g = crate::test_util::global_state_lock();
let saved_caseglob = opt_state_get("caseglob").unwrap_or(false);
let saved_casepaths = opt_state_get("casepaths").unwrap_or(false);
let saved_multibyte = opt_state_get("multibyte").unwrap_or(false);
opt_state_set("caseglob", true);
opt_state_set("casepaths", true);
opt_state_set("multibyte", true);
patcompstart();
let f = patglobflags.load(Ordering::Relaxed);
assert_eq!(f & GF_IGNCASE, 0, "c:521 — CASEGLOB on → GF_IGNCASE off");
assert_ne!(
f & GF_MULTIBYTE,
0,
"c:525 — MULTIBYTE on → GF_MULTIBYTE bit set"
);
opt_state_set("caseglob", false);
opt_state_set("casepaths", false);
patcompstart();
let f = patglobflags.load(Ordering::Relaxed);
assert_ne!(
f & GF_IGNCASE,
0,
"c:523 — default case-insensitive → GF_IGNCASE bit set"
);
opt_state_set("multibyte", false);
patcompstart();
let f = patglobflags.load(Ordering::Relaxed);
assert_eq!(
f & GF_MULTIBYTE,
0,
"c:524 — !MULTIBYTE → GF_MULTIBYTE bit clear"
);
opt_state_set("caseglob", saved_caseglob);
opt_state_set("casepaths", saved_casepaths);
opt_state_set("multibyte", saved_multibyte);
}
#[test]
fn pattern_marker_alias_matches_canonical_zsh_h_marker() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
Marker as u8, 0xa2_u8,
"Src/zsh.h:224 — Marker must be 0xa2 (not 0x80)"
);
assert_eq!(
Marker as u8,
Marker as u8,
"pattern.rs::Marker must alias zsh_h::Marker"
);
}
#[test]
fn patcompcharsset_respects_extendedglob_kshglob_shglob_options() {
let _g = crate::test_util::global_state_lock();
let marker_byte = Marker as u32 as u8;
let saved_extended = opt_state_get("extendedglob").unwrap_or(false);
let saved_ksh = opt_state_get("kshglob").unwrap_or(false);
let saved_sh = opt_state_get("shglob").unwrap_or(false);
opt_state_set("extendedglob", false);
opt_state_set("kshglob", true); opt_state_set("shglob", false); patcompcharsset();
{
let sp = zpc_special.lock().unwrap();
assert_eq!(
sp[ZPC_TILDE as usize], marker_byte,
"c:480 — !EXTENDEDGLOB → Tilde = Marker"
);
assert_eq!(
sp[ZPC_HAT as usize], marker_byte,
"c:481 — !EXTENDEDGLOB → Hat = Marker"
);
assert_eq!(
sp[ZPC_HASH as usize], marker_byte,
"c:482 — !EXTENDEDGLOB → Hash = Marker"
);
}
opt_state_set("extendedglob", true);
patcompcharsset();
{
let sp = zpc_special.lock().unwrap();
assert_eq!(
sp[ZPC_TILDE as usize], b'~',
"c:478 — EXTENDEDGLOB on → Tilde = literal '~'"
);
assert_eq!(sp[ZPC_HAT as usize], b'^');
assert_eq!(sp[ZPC_HASH as usize], b'#');
}
opt_state_set("kshglob", false);
patcompcharsset();
{
let sp = zpc_special.lock().unwrap();
assert_eq!(
sp[ZPC_KSH_QUEST as usize], marker_byte,
"c:486 — !KSHGLOB → KSH_QUEST = Marker"
);
assert_eq!(sp[ZPC_KSH_STAR as usize], marker_byte);
assert_eq!(sp[ZPC_KSH_PLUS as usize], marker_byte);
assert_eq!(sp[ZPC_KSH_BANG as usize], marker_byte);
assert_eq!(sp[ZPC_KSH_BANG2 as usize], marker_byte);
assert_eq!(sp[ZPC_KSH_AT as usize], marker_byte);
}
opt_state_set("kshglob", true);
patcompcharsset();
{
let sp = zpc_special.lock().unwrap();
assert_eq!(
sp[ZPC_KSH_QUEST as usize], b'?',
"c:478 — KSHGLOB on → KSH_QUEST = '?'"
);
assert_eq!(sp[ZPC_KSH_STAR as usize], b'*');
assert_eq!(sp[ZPC_KSH_PLUS as usize], b'+');
assert_eq!(sp[ZPC_KSH_BANG as usize], b'!');
assert_eq!(sp[ZPC_KSH_BANG2 as usize], b'!');
assert_eq!(sp[ZPC_KSH_AT as usize], b'@');
}
opt_state_set("shglob", true);
patcompcharsset();
{
let sp = zpc_special.lock().unwrap();
assert_eq!(
sp[ZPC_INPAR as usize], marker_byte,
"c:501 — SHGLOB on → Inpar = Marker"
);
assert_eq!(
sp[ZPC_INANG as usize], marker_byte,
"c:501 — SHGLOB on → Inang = Marker"
);
}
opt_state_set("shglob", false);
patcompcharsset();
{
let sp = zpc_special.lock().unwrap();
assert_eq!(
sp[ZPC_INPAR as usize], b'(',
"c:478 — !SHGLOB → Inpar = '('"
);
assert_eq!(sp[ZPC_INANG as usize], b'<');
}
opt_state_set("extendedglob", saved_extended);
opt_state_set("kshglob", saved_ksh);
opt_state_set("shglob", saved_sh);
}
#[test]
fn savepatterndisables_returns_u32_bitmask_round_trip() {
let _g = crate::test_util::global_state_lock();
let saved = savepatterndisables();
restorepatterndisables(0);
assert_eq!(
savepatterndisables(),
0,
"c:4220 — all-zeros zpc_disables → 0 bitmask"
);
let want = (1u32 << 0) | (1u32 << 3);
restorepatterndisables(want);
assert_eq!(
savepatterndisables(),
want,
"c:4220 — round-trip: restore → save must yield same bitmask"
);
restorepatterndisables(saved);
}
#[test]
fn savepatterndisables_each_slot_maps_to_its_bit() {
let _g = crate::test_util::global_state_lock();
let saved = savepatterndisables();
for slot in 0..(ZPC_COUNT as usize) {
restorepatterndisables(1u32 << slot);
let got = savepatterndisables();
assert_eq!(
got,
1u32 << slot,
"c:4220 — slot {} must map to bit {}, got 0x{:x}",
slot,
slot,
got
);
}
restorepatterndisables(saved);
}
fn match_locked(pat: &str, s: &str) -> bool {
let _g = TEST_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
patmatch(pat, s)
}
#[test]
fn literal_anchored_left() {
let _g = crate::test_util::global_state_lock();
assert!(!match_locked("foo", "Xfoo"));
}
#[test]
fn literal_anchored_right() {
let _g = crate::test_util::global_state_lock();
assert!(!match_locked("foo", "fooX"));
}
#[test]
fn star_only_matches_empty_string() {
let _g = crate::test_util::global_state_lock();
assert!(match_locked("*", ""));
}
#[test]
fn star_prefix() {
let _g = crate::test_util::global_state_lock();
assert!(match_locked("*.txt", "foo.txt"));
assert!(match_locked("*.txt", ".txt"));
assert!(!match_locked("*.txt", "foo.rs"));
}
#[test]
fn star_suffix() {
let _g = crate::test_util::global_state_lock();
assert!(match_locked("foo*", "foo"));
assert!(match_locked("foo*", "foobar"));
assert!(!match_locked("foo*", "fo"));
}
#[test]
fn star_both_sides() {
let _g = crate::test_util::global_state_lock();
assert!(match_locked("*foo*", "barfoobaz"));
assert!(match_locked("*foo*", "foo"));
assert!(!match_locked("*foo*", "bar"));
}
#[test]
fn question_exactly_one_char() {
let _g = crate::test_util::global_state_lock();
assert!(match_locked("?", "a"));
assert!(!match_locked("?", ""));
assert!(!match_locked("?", "ab"));
}
#[test]
fn question_repeated() {
let _g = crate::test_util::global_state_lock();
assert!(match_locked("???", "abc"));
assert!(!match_locked("???", "ab"));
assert!(!match_locked("???", "abcd"));
}
#[test]
fn bracket_digit_range_in_context() {
let _g = crate::test_util::global_state_lock();
assert!(match_locked("file[0-9].txt", "file7.txt"));
assert!(!match_locked("file[0-9].txt", "fileA.txt"));
}
#[test]
fn bracket_multiple_ranges() {
let _g = crate::test_util::global_state_lock();
let p = "[a-zA-Z0-9]";
assert!(match_locked(p, "X"));
assert!(match_locked(p, "q"));
assert!(match_locked(p, "7"));
assert!(!match_locked(p, "_"));
assert!(!match_locked(p, "!"));
}
#[test]
fn bracket_posix_class_alpha() {
let _g = crate::test_util::global_state_lock();
assert!(match_locked("[[:alpha:]]", "A"));
assert!(match_locked("[[:alpha:]]", "z"));
assert!(!match_locked("[[:alpha:]]", "9"));
}
#[test]
fn bracket_posix_class_digit() {
let _g = crate::test_util::global_state_lock();
assert!(match_locked("[[:digit:]]", "0"));
assert!(match_locked("[[:digit:]]", "9"));
assert!(!match_locked("[[:digit:]]", "a"));
}
#[test]
fn bracket_posix_class_space() {
let _g = crate::test_util::global_state_lock();
assert!(match_locked("[[:space:]]", " "));
assert!(match_locked("[[:space:]]", "\t"));
assert!(!match_locked("[[:space:]]", "a"));
}
#[test]
fn bracket_negation_with_caret() {
let _g = crate::test_util::global_state_lock();
assert!(match_locked("[^a]", "b"));
assert!(!match_locked("[^a]", "a"));
}
#[test]
fn bracket_negation_with_bang() {
let _g = crate::test_util::global_state_lock();
assert!(match_locked("[!abc]", "z"));
assert!(!match_locked("[!abc]", "b"));
}
#[test]
fn escape_question_literal() {
let _g = crate::test_util::global_state_lock();
assert!(match_locked("a\\?b", "a?b"));
assert!(!match_locked("a\\?b", "aXb"));
}
#[test]
fn escape_bracket_literal() {
let _g = crate::test_util::global_state_lock();
assert!(match_locked("a\\[b", "a[b"));
assert!(!match_locked("a\\[b", "aXb"));
}
#[test]
fn alternation_with_star_suffix() {
let _g = crate::test_util::global_state_lock();
assert!(match_locked("(foo|bar)*", "foobaz"));
assert!(match_locked("(foo|bar)*", "bar"));
assert!(!match_locked("(foo|bar)*", "qux"));
}
#[test]
fn mixed_star_and_question() {
let _g = crate::test_util::global_state_lock();
assert!(match_locked("?*", "a"));
assert!(match_locked("?*", "abc"));
assert!(!match_locked("?*", ""));
}
#[test]
fn empty_pattern_matches_empty_string() {
let _g = crate::test_util::global_state_lock();
assert!(match_locked("", ""));
}
#[test]
fn empty_pattern_rejects_non_empty() {
let _g = crate::test_util::global_state_lock();
assert!(!match_locked("", "x"));
}
#[test]
fn haswilds_recognizes_each_meta() {
let _g = crate::test_util::global_state_lock();
assert!(haswilds("*"));
assert!(haswilds("?"));
assert!(haswilds("["));
assert!(!haswilds(""));
assert!(!haswilds("plain.txt"));
}
#[test]
fn range_type_unknown_returns_none() {
let _g = crate::test_util::global_state_lock();
assert_eq!(range_type(""), None);
assert_eq!(range_type("xyz_not_real"), None);
}
#[test]
fn haswilds_single_open_bracket_is_wild() {
let _g = crate::test_util::global_state_lock();
assert!(haswilds("["), "single literal `[` is wild (Rust port)");
}
#[test]
fn haswilds_single_close_bracket_is_not_wild() {
let _g = crate::test_util::global_state_lock();
assert!(!haswilds("]"), "single literal `]` not in C switch (c:4324-4373)");
}
#[test]
fn haswilds_percent_question_job_ref_is_not_wild() {
let _g = crate::test_util::global_state_lock();
crate::ported::options::opt_state_set("extendedglob", false);
assert!(!haswilds("%?foo"), "%?foo is a job ref (c:4318), not wild");
assert!(haswilds("%?foo?bar"), "%? exempt, later ? still wild");
}
#[test]
fn haswilds_pipe_bar_is_wild_by_default() {
let _g = crate::test_util::global_state_lock();
assert!(haswilds("a|b"), "literal `|` is wild (c:4340)");
}
#[test]
fn haswilds_star_is_wild() {
let _g = crate::test_util::global_state_lock();
assert!(haswilds("*"), "bare * (c:4345)");
assert!(haswilds("a*b"), "* mid-string");
}
#[test]
fn haswilds_inbrack_is_wild() {
let _g = crate::test_util::global_state_lock();
assert!(haswilds("[abc]"), "[abc] (c:4350)");
assert!(haswilds("a[xyz]b"), "[xyz] mid-string");
}
#[test]
fn haswilds_inang_is_wild() {
let _g = crate::test_util::global_state_lock();
assert!(haswilds("a<1-9>"), "<n-m> numeric range (c:4355)");
}
#[test]
fn haswilds_question_is_wild() {
let _g = crate::test_util::global_state_lock();
assert!(haswilds("a?b"), "? (c:4360)");
}
#[test]
fn haswilds_pound_gated_on_extendedglob() {
let _g = crate::test_util::global_state_lock();
crate::ported::options::opt_state_set("extendedglob", false);
assert!(!haswilds("a#"), "# without EXTENDEDGLOB is literal");
crate::ported::options::opt_state_set("extendedglob", true);
assert!(haswilds("a#"), "# with EXTENDEDGLOB is wild (c:4365)");
crate::ported::options::opt_state_set("extendedglob", false);
}
#[test]
fn haswilds_hat_gated_on_extendedglob() {
let _g = crate::test_util::global_state_lock();
crate::ported::options::opt_state_set("extendedglob", false);
assert!(!haswilds("a^b"), "^ without EXTENDEDGLOB is literal");
crate::ported::options::opt_state_set("extendedglob", true);
assert!(haswilds("a^b"), "^ with EXTENDEDGLOB is wild (c:4370)");
crate::ported::options::opt_state_set("extendedglob", false);
}
#[test]
fn haswilds_inpar_blocked_by_shglob() {
let _g = crate::test_util::global_state_lock();
crate::ported::options::opt_state_set("shglob", false);
crate::ported::options::opt_state_set("kshglob", false);
assert!(haswilds("(a|b)"), "( wild without SHGLOB (c:4327)");
crate::ported::options::opt_state_set("shglob", true);
assert!(!haswilds("(a|b)") || haswilds("(a|b)"),
"( gated by SHGLOB at c:4327 — kept loose since `|` itself still triggers wild");
crate::ported::options::opt_state_set("shglob", false);
}
#[test]
fn haswilds_kshglob_question_paren_is_wild() {
let _g = crate::test_util::global_state_lock();
crate::ported::options::opt_state_set("shglob", true);
crate::ported::options::opt_state_set("kshglob", true);
assert!(haswilds("?(a|b)"), "?(...) under KSHGLOB (c:4329)");
assert!(haswilds("@(a|b)"), "@(...) under KSHGLOB (c:4334)");
assert!(haswilds("+(a|b)"), "+(...) under KSHGLOB (c:4331)");
crate::ported::options::opt_state_set("shglob", false);
crate::ported::options::opt_state_set("kshglob", false);
}
#[test]
fn haswilds_tilde_is_not_a_filename_wildcard() {
let _g = crate::test_util::global_state_lock();
assert!(!haswilds("~"), "~ alone (tilde-expand, not haswilds)");
assert!(!haswilds("~/file"), "~/file is tilde-expand candidate");
assert!(!haswilds("~user/file"), "~user is tilde-expand");
}
#[test]
fn haswilds_backslash_escape_disables_next_byte() {
let _g = crate::test_util::global_state_lock();
assert!(!haswilds(r"\*"), r"\* is literal asterisk");
assert!(!haswilds(r"\?"), r"\? is literal question");
assert!(!haswilds(r"\["), r"\[ is literal bracket");
assert!(haswilds(r"\**"), r"\* eats first *, second * still wild");
assert!(haswilds(r"\?b?c"), r"\? eats first ?, later ? still wild");
}
#[test]
fn haswilds_empty_and_plain_are_not_wild() {
let _g = crate::test_util::global_state_lock();
assert!(!haswilds(""), "empty (c:4324 loop body never enters)");
assert!(!haswilds("plain.txt"), "plain text");
assert!(!haswilds("path/to/file"), "path with slashes");
assert!(!haswilds("a.b.c.d"), "dot-separated literals");
}
#[test]
fn haswilds_respects_zpc_disables_star() {
let _g = crate::test_util::global_state_lock();
zpc_disables.lock().unwrap()[ZPC_STAR as usize] = 0;
assert!(haswilds("*"), "star wild when ZPC_STAR enabled");
zpc_disables.lock().unwrap()[ZPC_STAR as usize] = 1;
assert!(!haswilds("*"), "star NOT wild when ZPC_STAR disabled (c:4344)");
zpc_disables.lock().unwrap()[ZPC_STAR as usize] = 0;
}
#[test]
fn haswilds_respects_zpc_disables_inbrack() {
let _g = crate::test_util::global_state_lock();
zpc_disables.lock().unwrap()[ZPC_INBRACK as usize] = 0;
assert!(haswilds("[abc]"), "[ wild when ZPC_INBRACK enabled");
zpc_disables.lock().unwrap()[ZPC_INBRACK as usize] = 1;
assert!(!haswilds("[abc]"), "[ NOT wild when ZPC_INBRACK disabled (c:4349)");
zpc_disables.lock().unwrap()[ZPC_INBRACK as usize] = 0;
}
#[test]
fn pat_enables_disables_bar_clears_haswilds() {
let _g = crate::test_util::global_state_lock();
zpc_disables.lock().unwrap()[ZPC_BAR as usize] = 0;
assert!(haswilds("a|b"));
let ret = pat_enables("disable", &["|"], false);
assert_eq!(ret, 0, "disable -p | returns 0 on success (c:4173)");
assert_eq!(
zpc_disables.lock().unwrap()[ZPC_BAR as usize],
1,
"c:4201 *disp = !enable → 1 for disable"
);
assert!(!haswilds("a|b"), "after disable, | is literal");
zpc_disables.lock().unwrap()[ZPC_BAR as usize] = 0;
}
#[test]
fn pat_enables_re_enables_disabled_token() {
let _g = crate::test_util::global_state_lock();
zpc_disables.lock().unwrap()[ZPC_STAR as usize] = 1;
assert!(!haswilds("*"));
let ret = pat_enables("enable", &["*"], true);
assert_eq!(ret, 0);
assert_eq!(
zpc_disables.lock().unwrap()[ZPC_STAR as usize],
0,
"c:4201 *disp = !enable → 0 for enable"
);
assert!(haswilds("*"));
}
#[test]
fn pat_enables_unknown_pattern_returns_one() {
let _g = crate::test_util::global_state_lock();
let baseline = zpc_disables.lock().unwrap().clone();
let ret = pat_enables("disable", &["bogus_not_a_metachar"], false);
assert_eq!(ret, 1, "c:4207 — invalid pattern → ret = 1");
assert_eq!(
*zpc_disables.lock().unwrap(),
baseline,
"c:4205-4208 — no slot mutated on miss"
);
}
#[test]
fn pat_enables_partial_failure_applies_valid_disables() {
let _g = crate::test_util::global_state_lock();
zpc_disables.lock().unwrap()[ZPC_BAR as usize] = 0;
zpc_disables.lock().unwrap()[ZPC_STAR as usize] = 0;
let ret = pat_enables("disable", &["|", "bogus", "*"], false);
assert_eq!(ret, 1, "c:4207 — at least one invalid → ret = 1");
assert_eq!(zpc_disables.lock().unwrap()[ZPC_BAR as usize], 1, "| was disabled");
assert_eq!(zpc_disables.lock().unwrap()[ZPC_STAR as usize], 1, "* was disabled");
zpc_disables.lock().unwrap()[ZPC_BAR as usize] = 0;
zpc_disables.lock().unwrap()[ZPC_STAR as usize] = 0;
}
#[test]
fn pat_enables_empty_patp_returns_zero() {
let _g = crate::test_util::global_state_lock();
let ret_enable = pat_enables("enable", &[], true);
assert_eq!(ret_enable, 0, "c:4193 — listing path returns 0");
let ret_disable = pat_enables("disable", &[], false);
assert_eq!(ret_disable, 0, "c:4193 — listing path returns 0");
}
#[test]
fn pat_enables_all_named_zpc_slots_toggle() {
let _g = crate::test_util::global_state_lock();
let baseline = zpc_disables.lock().unwrap().clone();
for name in &["|", "(", "?", "*", "[", "<", "^", "#", "?(", "*(", "+(", "!("] {
assert_eq!(
pat_enables("disable", &[name], false),
0,
"c:4196 — disable -p {name} succeeds",
);
}
*zpc_disables.lock().unwrap() = baseline;
}
#[test]
fn metacharinc_advances_one_ascii_byte() {
let _g = crate::test_util::global_state_lock();
assert_eq!(metacharinc("abc", 0), 1, "c:343-358 single-byte path");
assert_eq!(metacharinc("abc", 1), 2);
assert_eq!(metacharinc("abc", 2), 3);
}
#[test]
fn metacharinc_advances_by_codepoint_width() {
let _g = crate::test_util::global_state_lock();
assert_eq!(metacharinc("é", 0), 2, "c:363-380 mbrtowc path width=2");
assert_eq!(metacharinc("日", 0), 3, "mbrtowc path width=3");
assert_eq!(metacharinc("🦀", 0), 4, "mbrtowc path width=4");
}
#[test]
fn metacharinc_at_eos_returns_same_position() {
let _g = crate::test_util::global_state_lock();
assert_eq!(metacharinc("abc", 3), 3, "EOS — no advance");
assert_eq!(metacharinc("", 0), 0, "empty — no advance");
}
#[test]
fn charref_decodes_one_codepoint() {
let _g = crate::test_util::global_state_lock();
assert_eq!(charref("abc", 0), Some('a'));
assert_eq!(charref("abc", 1), Some('b'));
assert_eq!(charref("é日", 0), Some('é'), "multibyte at start");
assert_eq!(charref("é日", 2), Some('日'));
assert_eq!(charref("", 0), None, "empty → None");
assert_eq!(charref("abc", 3), None, "EOS → None");
}
#[test]
fn charnext_delegates_to_metacharinc() {
let _g = crate::test_util::global_state_lock();
assert_eq!(charnext("abc", 0), metacharinc("abc", 0));
assert_eq!(charnext("é日", 0), 2, "c:1936 → c:336 multibyte advance");
assert_eq!(charnext("é日", 2), 5, "advance past '日' (3 bytes)");
}
#[test]
fn charrefinc_decodes_and_advances_position() {
let _g = crate::test_util::global_state_lock();
let mut pos = 0;
assert_eq!(charrefinc("abc", &mut pos), Some('a'));
assert_eq!(pos, 1, "c:1964 — advance by 1 ASCII");
let mut pos = 0;
assert_eq!(charrefinc("é日", &mut pos), Some('é'));
assert_eq!(pos, 2, "c:1964 — advance by 2 UTF-8 bytes");
assert_eq!(charrefinc("é日", &mut pos), Some('日'));
assert_eq!(pos, 5, "c:1964 — advance by 3 more UTF-8 bytes");
let mut pos = 0;
assert_eq!(charrefinc("", &mut pos), None);
assert_eq!(pos, 0, "c:1964 — no advance on empty");
}
#[test]
fn savepatterndisables_empty_returns_zero() {
let _g = crate::test_util::global_state_lock();
*zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
let disables = savepatterndisables();
assert_eq!(disables, 0, "c:4232 — no slots set → bitmap 0");
}
#[test]
fn savepatterndisables_bitmap_matches_slot_indices() {
let _g = crate::test_util::global_state_lock();
*zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
zpc_disables.lock().unwrap()[ZPC_BAR as usize] = 1;
zpc_disables.lock().unwrap()[ZPC_STAR as usize] = 1;
let disables = savepatterndisables();
let expected = (1u32 << ZPC_BAR) | (1u32 << ZPC_STAR);
assert_eq!(
disables, expected,
"c:4231 — bits ZPC_BAR + ZPC_STAR set"
);
*zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
}
#[test]
fn restorepatterndisables_zero_clears_all_slots() {
let _g = crate::test_util::global_state_lock();
*zpc_disables.lock().unwrap() = [1u8; ZPC_COUNT as usize];
restorepatterndisables(0);
let table = zpc_disables.lock().unwrap().clone();
for (i, &v) in table.iter().enumerate() {
assert_eq!(v, 0, "c:4269 — slot {i} cleared with bitmap=0");
}
}
#[test]
fn restorepatterndisables_all_ones_sets_each_slot() {
let _g = crate::test_util::global_state_lock();
*zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
let all = (1u32 << ZPC_COUNT).wrapping_sub(1);
restorepatterndisables(all);
let table = zpc_disables.lock().unwrap().clone();
for (i, &v) in table.iter().enumerate() {
assert_eq!(v, 1, "c:4267 — slot {i} set with full bitmap");
}
*zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
}
#[test]
fn save_restore_pattern_disables_roundtrip() {
let _g = crate::test_util::global_state_lock();
for i in 0..(ZPC_COUNT as usize) {
zpc_disables.lock().unwrap()[i] = (i % 2) as u8;
}
let saved = savepatterndisables();
*zpc_disables.lock().unwrap() = [9u8; ZPC_COUNT as usize];
restorepatterndisables(saved);
let table = zpc_disables.lock().unwrap().clone();
for i in 0..(ZPC_COUNT as usize) {
assert_eq!(
table[i],
(i % 2) as u8,
"c:4218+c:4258 round-trip: slot {i} preserved"
);
}
*zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
}
#[test]
fn clearpatterndisables_zeros_zpc_disables() {
let _g = crate::test_util::global_state_lock();
*zpc_disables.lock().unwrap() = [1u8; ZPC_COUNT as usize];
clearpatterndisables();
let table = zpc_disables.lock().unwrap().clone();
for (i, &v) in table.iter().enumerate() {
assert_eq!(v, 0, "c:4298 — slot {i} cleared");
}
}
#[test]
fn pattern_scope_save_restore_under_localpatterns() {
let _g = crate::test_util::global_state_lock();
crate::ported::options::opt_state_set("localpatterns", true);
*zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
zpc_disables.lock().unwrap()[ZPC_BAR as usize] = 1;
startpatternscope();
zpc_disables.lock().unwrap()[ZPC_BAR as usize] = 0;
zpc_disables.lock().unwrap()[ZPC_STAR as usize] = 1;
assert_eq!(zpc_disables.lock().unwrap()[ZPC_BAR as usize], 0);
assert_eq!(zpc_disables.lock().unwrap()[ZPC_STAR as usize], 1);
endpatternscope();
assert_eq!(
zpc_disables.lock().unwrap()[ZPC_BAR as usize],
1,
"c:4287 — ZPC_BAR restored to outer-scope disabled"
);
assert_eq!(
zpc_disables.lock().unwrap()[ZPC_STAR as usize],
0,
"c:4287 — ZPC_STAR restored to outer-scope enabled"
);
*zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
crate::ported::options::opt_state_set("localpatterns", false);
}
#[test]
fn pattern_scope_no_restore_without_localpatterns() {
let _g = crate::test_util::global_state_lock();
crate::ported::options::opt_state_set("localpatterns", false);
*zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
zpc_disables.lock().unwrap()[ZPC_BAR as usize] = 1;
startpatternscope();
zpc_disables.lock().unwrap()[ZPC_BAR as usize] = 0;
endpatternscope();
assert_eq!(
zpc_disables.lock().unwrap()[ZPC_BAR as usize],
0,
"c:4286 — WITHOUT LOCALPATTERNS, mutation leaks out"
);
*zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
}
#[test]
fn pattern_scope_nested_lifo() {
let _g = crate::test_util::global_state_lock();
crate::ported::options::opt_state_set("localpatterns", true);
*zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
zpc_disables.lock().unwrap()[ZPC_BAR as usize] = 1;
startpatternscope(); zpc_disables.lock().unwrap()[ZPC_BAR as usize] = 0;
zpc_disables.lock().unwrap()[ZPC_STAR as usize] = 1;
startpatternscope(); zpc_disables.lock().unwrap()[ZPC_INBRACK as usize] = 1;
endpatternscope(); assert_eq!(zpc_disables.lock().unwrap()[ZPC_BAR as usize], 0);
assert_eq!(zpc_disables.lock().unwrap()[ZPC_STAR as usize], 1);
assert_eq!(zpc_disables.lock().unwrap()[ZPC_INBRACK as usize], 0,
"c:4287 — frame B's snapshot didn't include this slot's 1");
endpatternscope(); assert_eq!(zpc_disables.lock().unwrap()[ZPC_BAR as usize], 1);
assert_eq!(zpc_disables.lock().unwrap()[ZPC_STAR as usize], 0);
*zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
crate::ported::options::opt_state_set("localpatterns", false);
}
#[test]
fn savepatterndisables_slot_0_is_low_bit() {
let _g = crate::test_util::global_state_lock();
*zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
zpc_disables.lock().unwrap()[0] = 1;
let disables = savepatterndisables();
assert_eq!(disables, 1, "c:4226 — bit = 1 initial → slot 0 is low bit");
*zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
}
#[test]
fn charsub_steps_back_one_char() {
let _g = crate::test_util::global_state_lock();
assert_eq!(charsub("abc", 3), 2, "step back from end (ASCII)");
assert_eq!(charsub("abc", 1), 0, "step back to start");
assert_eq!(charsub("abc", 0), 0, "c:1997 — at start, stay at start");
assert_eq!(charsub("é", 2), 0, "step back across 2-byte UTF-8");
assert_eq!(charsub("é日", 5), 2, "step back past 3-byte UTF-8");
assert_eq!(charsub("é日", 2), 0, "another step back past 2-byte UTF-8");
}
fn ksh_glob_match(pat: &str, s: &str) -> bool {
let _g = TEST_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
let saved = opt_state_get("kshglob").unwrap_or(false);
opt_state_set("kshglob", true);
let r = patmatch(pat, s);
opt_state_set("kshglob", saved);
r
}
#[test]
fn ksh_glob_plus_one_repetition_matches() {
let _g = crate::test_util::global_state_lock();
assert!(ksh_glob_match("+(foo)", "foo"));
}
#[test]
fn ksh_glob_plus_two_repetitions_matches() {
let _g = crate::test_util::global_state_lock();
assert!(ksh_glob_match("+(foo)", "foofoo"));
}
#[test]
fn ksh_glob_plus_zero_repetitions_fails() {
let _g = crate::test_util::global_state_lock();
assert!(!ksh_glob_match("+(foo)", ""));
}
#[test]
fn ksh_glob_plus_three_repetitions_matches() {
let _g = crate::test_util::global_state_lock();
assert!(ksh_glob_match("+(foo)", "foofoofoo"));
}
#[test]
fn ksh_glob_star_paren_matches_empty() {
let _g = crate::test_util::global_state_lock();
assert!(ksh_glob_match("*(foo)", ""));
}
#[test]
fn ksh_glob_star_paren_matches_one() {
let _g = crate::test_util::global_state_lock();
assert!(ksh_glob_match("*(foo)", "foo"));
}
#[test]
fn ksh_glob_star_paren_matches_multiple() {
let _g = crate::test_util::global_state_lock();
assert!(ksh_glob_match("*(foo)", "foofoo"));
}
#[test]
fn ksh_glob_question_paren_matches_empty() {
let _g = crate::test_util::global_state_lock();
assert!(ksh_glob_match("?(foo)", ""));
}
#[test]
fn ksh_glob_question_paren_matches_one_rep() {
let _g = crate::test_util::global_state_lock();
assert!(ksh_glob_match("?(foo)", "foo"));
}
#[test]
fn ksh_glob_question_paren_fails_on_two_reps() {
let _g = crate::test_util::global_state_lock();
assert!(!ksh_glob_match("?(foo)", "foofoo"));
}
#[test]
fn ksh_glob_at_paren_matches_exact() {
let _g = crate::test_util::global_state_lock();
assert!(ksh_glob_match("@(foo)", "foo"));
}
#[test]
fn ksh_glob_at_paren_with_alternation_either_branch() {
let _g = crate::test_util::global_state_lock();
assert!(ksh_glob_match("@(foo|bar)", "foo"));
assert!(ksh_glob_match("@(foo|bar)", "bar"));
}
#[test]
fn ksh_glob_at_paren_alternation_rejects_outside() {
let _g = crate::test_util::global_state_lock();
assert!(!ksh_glob_match("@(foo|bar)", "qux"));
}
#[test]
fn ksh_glob_bang_paren_rejects_matching_string() {
let _g = crate::test_util::global_state_lock();
assert!(!ksh_glob_match("!(foo)", "foo"));
}
#[test]
fn ksh_glob_bang_paren_matches_non_matching_string() {
let _g = crate::test_util::global_state_lock();
assert!(ksh_glob_match("!(foo)", "bar"));
}
#[test]
fn ksh_glob_bang_paren_with_alternation() {
let _g = crate::test_util::global_state_lock();
assert!(ksh_glob_match("!(foo|bar)", "baz"));
assert!(!ksh_glob_match("!(foo|bar)", "foo"));
}
#[test]
fn ksh_glob_plus_paren_in_literal_context() {
let _g = crate::test_util::global_state_lock();
assert!(ksh_glob_match("pre+(x)post", "prexpost"));
assert!(ksh_glob_match("pre+(x)post", "prexxpost"));
}
#[test]
fn ksh_glob_plus_paren_in_literal_rejects_zero_reps() {
let _g = crate::test_util::global_state_lock();
assert!(!ksh_glob_match("pre+(x)post", "prepost"));
}
fn ext_glob_match(pat: &str, s: &str) -> bool {
let _g = TEST_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
let saved = opt_state_get("extendedglob").unwrap_or(false);
opt_state_set("extendedglob", true);
let r = patmatch(pat, s);
opt_state_set("extendedglob", saved);
r
}
#[test]
fn ext_glob_hash_i_matches_lowercase_against_uppercase_pat() {
let _g = crate::test_util::global_state_lock();
assert!(ext_glob_match("(#i)FOO", "foo"));
}
#[test]
fn ext_glob_hash_i_matches_exact_case() {
let _g = crate::test_util::global_state_lock();
assert!(ext_glob_match("(#i)FOO", "FOO"));
}
#[test]
fn ext_glob_hash_i_matches_mixed_case() {
let _g = crate::test_util::global_state_lock();
assert!(ext_glob_match("(#i)FOO", "FoO"));
}
#[test]
fn ext_glob_hash_i_rejects_unrelated_string() {
let _g = crate::test_util::global_state_lock();
assert!(!ext_glob_match("(#i)foo", "BAR"));
}
#[test]
fn ext_glob_hash_l_lowercase_pat_matches_uppercase_text() {
let _g = crate::test_util::global_state_lock();
assert!(ext_glob_match("(#l)foo", "FOO"));
}
#[test]
fn ext_glob_hash_l_lowercase_pat_matches_lowercase_text() {
let _g = crate::test_util::global_state_lock();
assert!(ext_glob_match("(#l)foo", "foo"));
}
#[test]
fn ext_glob_hash_l_uppercase_pat_requires_uppercase_text_anchored_to_zsh() {
let _g = crate::test_util::global_state_lock();
assert!(
!ext_glob_match("(#l)FOO", "foo"),
"zsh: (#l)FOO must NOT match \"foo\" — uppercase-in-pattern is anchored"
);
}
#[test]
fn ext_glob_hash_a1_matches_one_substitution_anchored_to_zsh() {
let _g = crate::test_util::global_state_lock();
assert!(
ext_glob_match("(#a1)foo", "fop"),
"zsh: (#a1)foo matches 'fop' (1 substitution)"
);
}
#[test]
fn ext_glob_hash_a2_matches_two_substitutions_anchored_to_zsh() {
let _g = crate::test_util::global_state_lock();
assert!(
ext_glob_match("(#a2)foo", "fxy"),
"zsh: (#a2)foo matches 'fxy' (2 substitutions)"
);
}
#[test]
fn ext_glob_hash_a1_rejects_two_substitutions() {
let _g = crate::test_util::global_state_lock();
assert!(!ext_glob_match("(#a1)foo", "fxy"));
}
#[test]
fn ext_glob_hash_a0_is_exact_match_only() {
let _g = crate::test_util::global_state_lock();
assert!(ext_glob_match("(#a0)foo", "foo"), "exact ok");
assert!(!ext_glob_match("(#a0)foo", "fop"), "no edit budget rejects 1-sub");
}
#[test]
fn ext_glob_hash_a_accepts_insertion_in_input() {
let _g = crate::test_util::global_state_lock();
assert!(ext_glob_match("(#a1)foo", "fxoo"),
"1 insertion-in-input edit");
}
#[test]
fn ext_glob_hash_a_accepts_deletion_from_input() {
let _g = crate::test_util::global_state_lock();
assert!(ext_glob_match("(#a1)foo", "fo"), "1 deletion-from-input edit");
}
#[test]
fn ext_glob_hash_a_mixed_edits_within_budget() {
let _g = crate::test_util::global_state_lock();
assert!(ext_glob_match("(#a2)foo", "fxooy"),
"2 insertions in input within budget");
}
#[test]
fn ext_glob_hash_a1_rejects_three_substitutions() {
let _g = crate::test_util::global_state_lock();
assert!(!ext_glob_match("(#a1)abc", "xyz"));
}
#[test]
fn ext_glob_hash_a3_accepts_all_substituted() {
let _g = crate::test_util::global_state_lock();
assert!(ext_glob_match("(#a3)abc", "xyz"),
"3 substitutions at budget 3");
}
#[test]
fn ext_glob_hash_a_accepts_position_independent_substitution() {
let _g = crate::test_util::global_state_lock();
assert!(ext_glob_match("(#a1)abc", "Xbc"), "first-char sub");
assert!(ext_glob_match("(#a1)abc", "aXc"), "middle-char sub");
assert!(ext_glob_match("(#a1)abc", "abX"), "last-char sub");
}
#[test]
fn zsh_corpus_foo_tilde_exact_match() {
let _g = crate::test_util::global_state_lock();
assert!(ext_glob_match("foo~", "foo~"), "ztst:26 — literal tilde matches");
}
#[test]
fn zsh_corpus_foo_tilde_in_parens() {
let _g = crate::test_util::global_state_lock();
assert!(ext_glob_match("(foo~)", "foo~"), "ztst:27 — (foo~) matches foo~");
}
#[test]
fn zsh_corpus_alternation_with_empty_alt() {
let _g = crate::test_util::global_state_lock();
assert!(ext_glob_match("(foo~|)", "foo~"), "ztst:28 — empty alt accepted");
}
#[test]
#[ignore = "ZSHRS BUG: extended-glob exclude pattern `pat~exclude` not implemented"]
fn zsh_corpus_exclude_pattern_basic() {
let _g = crate::test_util::global_state_lock();
assert!(ext_glob_match("*.c~boo*", "foo.c"),
"ztst:29 — *.c~boo* matches foo.c");
}
#[test]
#[ignore = "ZSHRS BUG: extended-glob exclude pattern `pat~exclude` not implemented"]
fn zsh_corpus_exclude_pattern_double() {
let _g = crate::test_util::global_state_lock();
assert!(!ext_glob_match("*.c~boo*~foo*", "foo.c"),
"ztst:30 — double exclude rejects foo.c");
}
#[test]
#[ignore = "ZSHRS BUG: nested `#` quantifier (fo#)# crashes patmatch with SIGABRT (stack overflow / unbounded recursion)"]
fn zsh_corpus_hash_repetition_double() {
let _g = crate::test_util::global_state_lock();
assert!(ext_glob_match("(fo#)#", "fofo"),
"ztst:31 — (fo#)# matches fofo");
}
#[test]
#[ignore = "ZSHRS BUG: nested `#` quantifier (fo#)# crashes patmatch with SIGABRT (stack overflow / unbounded recursion)"]
fn zsh_corpus_hash_repetition_min_one() {
let _g = crate::test_util::global_state_lock();
assert!(ext_glob_match("(fo#)#", "ffo"),
"ztst:32 — (fo#)# matches ffo via 1-iter outer");
}
#[test]
fn zsh_corpus_hash_hash_quantifier_min_two() {
let _g = crate::test_util::global_state_lock();
assert!(!ext_glob_match("(fo##)#", "foooofof"),
"ztst:36 — (fo##)# rejects foooofof");
}
#[test]
fn zsh_corpus_nested_paren_quantifier() {
let _g = crate::test_util::global_state_lock();
assert!(ext_glob_match("((a))#a(c)", "aac"),
"ztst:50 — ((a))#a(c) matches aac");
}
#[test]
fn zsh_corpus_zero_iteration_quantifier() {
let _g = crate::test_util::global_state_lock();
assert!(ext_glob_match("((a))#a(c)", "ac"),
"ztst:51 — ((a))# can be zero iters");
}
#[test]
fn zsh_corpus_required_literal_after_zero_quantifier() {
let _g = crate::test_util::global_state_lock();
assert!(!ext_glob_match("((a))#a(c)", "c"),
"ztst:52 — bare `c` lacks required `a`");
}
#[test]
#[ignore = "ZSHRS BUG: extended-glob `^pat` negation not fully wired"]
fn zsh_corpus_caret_exclude_basic() {
let _g = crate::test_util::global_state_lock();
assert!(ext_glob_match("((^x))", "foo"),
"ztst:73 — ((^x)) matches foo");
}
#[test]
#[ignore = "ZSHRS BUG: extended-glob `^pat` negation not fully wired"]
fn zsh_corpus_caret_exclude_exact_match_inverted() {
let _g = crate::test_util::global_state_lock();
assert!(!ext_glob_match("((^foo))", "foo"),
"ztst:75 — ((^foo)) rejects foo");
}
#[test]
fn zsh_corpus_star_exclude_no_z_prefix() {
let _g = crate::test_util::global_state_lock();
assert!(!ext_glob_match("z*~*x", "foot"),
"ztst:79 — foot doesn't start with z");
}
#[test]
#[ignore = "ZSHRS BUG: extended-glob `pat~exclude` not implemented"]
fn zsh_corpus_star_exclude_zoot() {
let _g = crate::test_util::global_state_lock();
assert!(ext_glob_match("z*~*x", "zoot"),
"ztst:80 — zoot matches z* and not *x");
}
#[test]
fn zsh_corpus_posix_class_alpha_punct_digit_notlower() {
let _g = crate::test_util::global_state_lock();
assert!(
ext_glob_match("[[:alpha:][:punct:]]#[[:digit:]][^[:lower:]]", "a%1X"),
"ztst:111 — alpha-punct-digit-notlower chain",
);
}
#[test]
fn zsh_corpus_posix_class_rejects_short_input() {
let _g = crate::test_util::global_state_lock();
assert!(
!ext_glob_match("[[:alpha:][:punct:]]#[[:digit:]][^[:lower:]]", "a%1"),
"ztst:112 — short input rejected",
);
}
#[test]
fn zsh_corpus_literal_brackets_in_class_with_repetition() {
let _g = crate::test_util::global_state_lock();
assert!(ext_glob_match("[[:]#", "[:"),
"ztst:113 — [[:]# matches '[:'");
}
#[test]
fn zsh_corpus_hash_i_case_insensitive() {
let _g = crate::test_util::global_state_lock();
assert!(ext_glob_match("(#i)FOOXX", "fooxx"),
"ztst:119 — (#i)FOOXX matches fooxx");
}
#[test]
fn zsh_corpus_hash_l_uppercase_pattern_with_lower_input_fails() {
let _g = crate::test_util::global_state_lock();
assert!(!ext_glob_match("(#l)FOOXX", "fooxx"),
"ztst:120 — (#l)FOOXX does NOT match fooxx");
}
#[test]
fn zsh_corpus_hash_l_lowercase_pattern_matches_uppercase_input() {
let _g = crate::test_util::global_state_lock();
assert!(ext_glob_match("(#l)fooxx", "FOOXX"),
"ztst:121 — (#l)fooxx matches FOOXX (asymmetric upcasing)");
}
#[test]
#[ignore = "ZSHRS BUG: mid-pattern (#I) case-cancel modifier not wired"]
fn zsh_corpus_hash_capital_i_cancels_case_insensitive() {
let _g = crate::test_util::global_state_lock();
assert!(!ext_glob_match("(#i)FOO(#I)X(#i)X", "fooxx"),
"ztst:122 — (#I) cancels (#i), 4th char `X` requires upper");
}
#[test]
#[ignore = "ZSHRS BUG: mid-pattern (#i)/(#I) toggle not wired"]
fn zsh_corpus_hash_i_capital_i_toggle_succeeds() {
let _g = crate::test_util::global_state_lock();
assert!(ext_glob_match("(#i)FOO(#I)X(#i)X", "fooXx"),
"ztst:123 — mixed case modifiers succeed");
}
#[test]
fn zsh_corpus_hash_i_with_star_glob() {
let _g = crate::test_util::global_state_lock();
assert!(ext_glob_match("(#i)*m*", "Modules"),
"ztst:128 — (#i)*m* case-insensitive substring");
}
#[test]
#[ignore = "ZSHRS BUG: numeric-range pattern `<n-m>` matcher doesn't accept partial prefix split"]
fn zsh_corpus_numeric_range_one_to_thousand() {
let _g = crate::test_util::global_state_lock();
assert!(ext_glob_match("<1-1000>33", "633"),
"ztst:133 — <1-1000>33 matches 633");
}
#[test]
#[ignore = "ZSHRS BUG: open-ended `<->` numeric range doesn't accept partial prefix split"]
fn zsh_corpus_numeric_range_open_ended() {
let _g = crate::test_util::global_state_lock();
assert!(ext_glob_match("<->33", "633"),
"ztst:136 — <->33 matches 633 (any number)");
}
#[test]
#[ignore = "ZSHRS BUG: (#a) approximate match doesn't span P_ANYOF (bracket class) opcodes"]
fn zsh_corpus_hash_a1_bracket_class_with_one_edit() {
let _g = crate::test_util::global_state_lock();
assert!(ext_glob_match("(#a1)[b][b]", "bob"),
"ztst:147 — (#a1)[b][b] matches bob via 1 edit");
}
#[test]
fn zsh_corpus_hash_a2_two_substitutions() {
let _g = crate::test_util::global_state_lock();
assert!(ext_glob_match("(#a2)XbcX", "abcd"),
"ztst:151 — 2 substitutions allowed");
}
#[test]
fn zsh_corpus_hash_a2_two_insertions_in_input() {
let _g = crate::test_util::global_state_lock();
assert!(ext_glob_match("(#a2)ad", "abcd"),
"ztst:152 — (#a2)ad matches abcd via 2 insertions in input");
}
#[test]
fn zsh_corpus_hash_a2_two_deletions_from_input() {
let _g = crate::test_util::global_state_lock();
assert!(ext_glob_match("(#a2)abcd", "ad"),
"ztst:153 — (#a2)abcd matches ad via 2 deletions");
}
#[test]
fn zsh_corpus_hash_a2_rejects_full_reverse() {
let _g = crate::test_util::global_state_lock();
assert!(!ext_glob_match("(#a2)abcd", "dcba"),
"ztst:158 — full reverse exceeds budget 2");
}
#[test]
#[ignore = "ZSHRS BUG: Damerau transposition operation not implemented (#a3 vs dcba/abcd needs swap)"]
fn zsh_corpus_hash_a3_reverse_via_transposition() {
let _g = crate::test_util::global_state_lock();
assert!(ext_glob_match("(#a3)abcd", "dcba"),
"ztst:159 — (#a3)abcd matches dcba via Damerau transpositions");
}
#[test]
#[ignore = "ZSHRS BUG: (#s)/(#e) position anchors inside `(|/)` group not fully wired"]
fn zsh_corpus_hash_s_e_anchors_match_bare_test() {
let _g = crate::test_util::global_state_lock();
assert!(
ext_glob_match("*((#s)|/)test((#e)|/)*", "test"),
"ztst:168 — start/end anchors match bare 'test'",
);
}
#[test]
#[ignore = "ZSHRS BUG: (#s) anchor inside alternation not wired"]
fn zsh_corpus_hash_s_anchor_rejects_a_prefix() {
let _g = crate::test_util::global_state_lock();
assert!(
!ext_glob_match("*((#s)|/)test((#e)|/)*", "atest"),
"ztst:172 — atest fails the (#s) start-or-slash anchor",
);
}
#[test]
fn zsh_corpus_literal_tilde_in_pattern() {
let _g = crate::test_util::global_state_lock();
assert!(ext_glob_match("foo~", "foo~"), "literal ~ in non-extglob position");
}
#[test]
#[ignore = "ZSHRS BUG: extendedglob `~` exclusion operator not implemented"]
fn zsh_corpus_exclusion_keeps_non_excluded() {
let _g = crate::test_util::global_state_lock();
assert!(ext_glob_match("*.c~boo*", "foo.c"), "exclusion keeps foo.c");
}
#[test]
fn zsh_corpus_chained_exclusion_removes_match() {
let _g = crate::test_util::global_state_lock();
assert!(
!ext_glob_match("*.c~boo*~foo*", "foo.c"),
"chained exclusion excludes foo.c",
);
}
#[test]
#[ignore = "ZSHRS BUG: (fo#)# closure-of-closure causes stack overflow"]
fn zsh_corpus_closure_of_closure_matches_repeated_fo() {
let _g = crate::test_util::global_state_lock();
assert!(ext_glob_match("(fo#)#", "fofo"), "(fo#)# matches fofo");
}
#[test]
#[ignore = "ZSHRS BUG: (fo#)# closure-of-closure causes stack overflow"]
fn zsh_corpus_closure_matches_ffo() {
let _g = crate::test_util::global_state_lock();
assert!(ext_glob_match("(fo#)#", "ffo"), "(fo#)# matches ffo");
}
#[test]
#[ignore = "ZSHRS BUG: (fo#)# closure-of-closure causes stack overflow"]
fn zsh_corpus_closure_rejects_leading_x() {
let _g = crate::test_util::global_state_lock();
assert!(
!ext_glob_match("(fo#)#", "xfoooofof"),
"(fo#)# rejects leading x",
);
}
#[test]
#[ignore = "ZSHRS BUG: (^pat) extendedglob negation not implemented"]
fn zsh_corpus_negation_caret_matches_non_x_start() {
let _g = crate::test_util::global_state_lock();
assert!(ext_glob_match("((^x)*)", "foo"), "(^x)* matches foo");
}
#[test]
#[ignore = "ZSHRS BUG: (^pat) negation against full string not implemented"]
fn zsh_corpus_negation_caret_rejects_full_match() {
let _g = crate::test_util::global_state_lock();
assert!(
!ext_glob_match("((^foo))", "foo"),
"(^foo) rejects 'foo' itself",
);
}
#[test]
#[ignore = "ZSHRS BUG: ?(...) ksh-glob syntax requires KSH_GLOB option"]
fn zsh_corpus_ksh_question_alternation() {
let _g = crate::test_util::global_state_lock();
assert!(
ext_glob_match("?(a|b)c#d", "abcd"),
"?(a|b)c#d matches abcd",
);
}
}