diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-06-14 12:04:51 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-06-14 12:14:41 +0900 |
| commit | 4ace644474d3d0e5575d19616ce46ba7d521fc64 (patch) | |
| tree | 66a100c91058c17a14df3173b97360aaf806274f /crates/shirabe-external-packages/src | |
| parent | 716f44031a39c5e43fb441ecc470db76efc23dd4 (diff) | |
| download | php-shirabe-4ace644474d3d0e5575d19616ce46ba7d521fc64.tar.gz php-shirabe-4ace644474d3d0e5575d19616ce46ba7d521fc64.tar.zst php-shirabe-4ace644474d3d0e5575d19616ce46ba7d521fc64.zip | |
refactor(pcre): move preg_* free functions into php-shim
The free preg_* functions, the CaptureKey type, and the PREG_* constants
move from the pcre crate into shirabe-php-shim, where the rest of the
PHP standard-library shims live. Colliding names take a 2 suffix
(preg_match2, preg_split2, etc.) since the shim already exposes
differently-shaped variants. Preg keeps its public API and delegates to
the relocated functions; CaptureKey is re-exported from composer::pcre
so consumers are unaffected.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'crates/shirabe-external-packages/src')
| -rw-r--r-- | crates/shirabe-external-packages/src/composer/pcre/preg.rs | 368 |
1 files changed, 15 insertions, 353 deletions
diff --git a/crates/shirabe-external-packages/src/composer/pcre/preg.rs b/crates/shirabe-external-packages/src/composer/pcre/preg.rs index 2f8c46b..f72835d 100644 --- a/crates/shirabe-external-packages/src/composer/pcre/preg.rs +++ b/crates/shirabe-external-packages/src/composer/pcre/preg.rs @@ -10,15 +10,11 @@ //! distinguishes participating from non-participating groups. use indexmap::IndexMap; +use shirabe_php_shim::{ + PREG_OFFSET_CAPTURE, PREG_SET_ORDER, PREG_SPLIT_OFFSET_CAPTURE, PREG_UNMATCHED_AS_NULL, +}; -const PREG_PATTERN_ORDER: i64 = 1; -const PREG_SET_ORDER: i64 = 2; -const PREG_OFFSET_CAPTURE: i64 = 256; -const PREG_UNMATCHED_AS_NULL: i64 = 512; -const PREG_SPLIT_NO_EMPTY: i64 = 1; -const PREG_SPLIT_DELIM_CAPTURE: i64 = 2; -const PREG_SPLIT_OFFSET_CAPTURE: i64 = 4; -const PREG_GREP_INVERT: i64 = 1; +pub use shirabe_php_shim::CaptureKey; #[derive(Debug)] pub struct Preg; @@ -42,7 +38,7 @@ impl Preg { Self::check_offset_capture(flags, "matchWithOffsets"); let mut internal: IndexMap<CaptureKey, Option<String>> = IndexMap::new(); - let result = preg_match( + let result = shirabe_php_shim::preg_match2( pattern, subject, Some(&mut internal), @@ -81,7 +77,7 @@ impl Preg { Self::check_set_order(flags); let mut internal: IndexMap<CaptureKey, Vec<Option<String>>> = IndexMap::new(); - let result = preg_match_all( + let result = shirabe_php_shim::preg_match_all2( pattern, subject, Some(&mut internal), @@ -107,7 +103,7 @@ impl Preg { Self::check_set_order(flags); let mut internal: IndexMap<CaptureKey, Vec<(Option<String>, i64)>> = IndexMap::new(); - let result = preg_match_all_offset_capture( + let result = shirabe_php_shim::preg_match_all_offset_capture2( pattern, subject, Some(&mut internal), @@ -151,7 +147,8 @@ impl Preg { // `$subject` is statically a string here, so the is_scalar/is_array // guards (ARRAY_MSG / INVALID_TYPE_MSG) of the PHP original are // unreachable and not reproduced. - preg_replace(pattern, replacement, subject, limit, count).unwrap_or_else(|| invalid_regex()) + shirabe_php_shim::preg_replace2(pattern, replacement, subject, limit, count) + .unwrap_or_else(|| invalid_regex()) } pub fn replace_callback<F: FnMut(&IndexMap<CaptureKey, String>) -> String>( @@ -174,7 +171,7 @@ impl Preg { replacement(&drop_null_matches_ref(internal)) }; - preg_replace_callback(pattern, adapter, subject, limit, count, flags) + shirabe_php_shim::preg_replace_callback2(pattern, adapter, subject, limit, count, flags) .unwrap_or_else(|| invalid_regex()) } @@ -188,7 +185,8 @@ impl Preg { "PREG_SPLIT_OFFSET_CAPTURE is not supported as it changes the type of $matches, use splitWithOffsets() instead" ); - preg_split(pattern, subject, limit, flags).unwrap_or_else(|| invalid_regex()) + shirabe_php_shim::preg_split2(pattern, subject, limit, flags) + .unwrap_or_else(|| invalid_regex()) } pub fn grep(pattern: &str, array: &[&str]) -> Vec<String> { @@ -196,7 +194,7 @@ impl Preg { } pub fn grep3(pattern: &str, array: &[&str], flags: i64) -> Vec<String> { - preg_grep(pattern, array, flags).unwrap_or_else(|| invalid_regex()) + shirabe_php_shim::preg_grep2(pattern, array, flags).unwrap_or_else(|| invalid_regex()) } pub fn is_match(pattern: &str, subject: &str) -> bool { @@ -227,7 +225,7 @@ impl Preg { matches: &mut IndexMap<String, String>, ) -> bool { let mut internal: IndexMap<CaptureKey, Option<String>> = IndexMap::new(); - let result = preg_match( + let result = shirabe_php_shim::preg_match2( pattern, subject, Some(&mut internal), @@ -250,7 +248,7 @@ impl Preg { // Classic preg_match semantics (no PREG_UNMATCHED_AS_NULL): trailing // unmatched groups are truncated, interior unmatched groups become "". let mut internal: IndexMap<CaptureKey, Option<String>> = IndexMap::new(); - let result = preg_match(pattern, subject, Some(&mut internal), 0, 0) + let result = shirabe_php_shim::preg_match2(pattern, subject, Some(&mut internal), 0, 0) .unwrap_or_else(|| invalid_regex()); if result == 0 { @@ -366,342 +364,6 @@ fn null_to_empty_offset_match_all( .collect() } -#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Hash)] -pub enum CaptureKey { - ByIndex(usize), - ByName(String), -} - -// Returns Some(0|1) on success or None when the underlying preg_match returned -// false. Unmatched groups are reported as None (PREG_UNMATCHED_AS_NULL). -pub fn preg_match( - pattern: &str, - subject: &str, - matches: Option<&mut IndexMap<CaptureKey, Option<String>>>, - flags: i64, - offset: usize, -) -> Option<i64> { - let re = shirabe_php_shim::compile_php_pattern(pattern).ok()?; - let unmatched_as_null = flags & PREG_UNMATCHED_AS_NULL != 0; - let caps = re.captures_at(subject, offset); - - if let Some(out) = matches { - out.clear(); - if let Some(caps) = &caps { - let names: Vec<Option<&str>> = re.capture_names().collect(); - *out = single_match_map(caps, &names, unmatched_as_null); - } - } - - Some(if caps.is_some() { 1 } else { 0 }) -} - -pub fn preg_match_all( - pattern: &str, - subject: &str, - matches: Option<&mut IndexMap<CaptureKey, Vec<Option<String>>>>, - flags: i64, - offset: usize, -) -> Option<i64> { - let re = shirabe_php_shim::compile_php_pattern(pattern).ok()?; - let unmatched_as_null = flags & PREG_UNMATCHED_AS_NULL != 0; - let group_count = re.captures_len(); - let names: Vec<Option<&str>> = re.capture_names().collect(); - - // PREG_PATTERN_ORDER: one column per group, one row per match occurrence. - let mut groups: Vec<Vec<Option<String>>> = vec![Vec::new(); group_count]; - let mut count = 0i64; - for caps in re.captures_iter(&subject[offset..]) { - count += 1; - for (g, column) in groups.iter_mut().enumerate() { - let value = caps.get(g).map(|m| m.as_str().to_string()); - column.push(if unmatched_as_null { - value - } else { - Some(value.unwrap_or_default()) - }); - } - } - - if let Some(out) = matches { - out.clear(); - for (g, column) in groups.into_iter().enumerate() { - if let Some(Some(name)) = names.get(g) { - out.insert(CaptureKey::ByName((*name).to_string()), column.clone()); - } - out.insert(CaptureKey::ByIndex(g), column); - } - } - - Some(count) -} - -pub fn preg_match_all_offset_capture( - pattern: &str, - subject: &str, - matches: Option<&mut IndexMap<CaptureKey, Vec<(Option<String>, i64)>>>, - flags: i64, - offset: usize, -) -> Option<i64> { - let re = shirabe_php_shim::compile_php_pattern(pattern).ok()?; - let unmatched_as_null = flags & PREG_UNMATCHED_AS_NULL != 0; - let group_count = re.captures_len(); - let names: Vec<Option<&str>> = re.capture_names().collect(); - - let mut groups: Vec<Vec<(Option<String>, i64)>> = vec![Vec::new(); group_count]; - let mut count = 0i64; - for caps in re.captures_iter(&subject[offset..]) { - count += 1; - for (g, column) in groups.iter_mut().enumerate() { - let entry = match caps.get(g) { - Some(m) => (Some(m.as_str().to_string()), (m.start() + offset) as i64), - None if unmatched_as_null => (None, -1), - None => (Some(String::new()), -1), - }; - column.push(entry); - } - } - - if let Some(out) = matches { - out.clear(); - for (g, column) in groups.into_iter().enumerate() { - if let Some(Some(name)) = names.get(g) { - out.insert(CaptureKey::ByName((*name).to_string()), column.clone()); - } - out.insert(CaptureKey::ByIndex(g), column); - } - } - - Some(count) -} - -pub fn preg_replace( - pattern: &str, - replacement: &str, - subject: &str, - limit: i64, - count: Option<&mut usize>, -) -> Option<String> { - let re = shirabe_php_shim::compile_php_pattern(pattern).ok()?; - let limit = if limit < 0 { - usize::MAX - } else { - limit as usize - }; - - let mut out: Vec<u8> = Vec::new(); - let mut last = 0usize; - let mut n = 0usize; - for caps in re.captures_iter(subject) { - if n >= limit { - break; - } - let m = caps.get(0).unwrap(); - out.extend_from_slice(&subject.as_bytes()[last..m.start()]); - expand_php_replacement(replacement, &caps, &mut out); - last = m.end(); - n += 1; - } - out.extend_from_slice(&subject.as_bytes()[last..]); - - if let Some(count) = count { - *count = n; - } - Some(String::from_utf8_lossy(&out).into_owned()) -} - -pub fn preg_replace_callback<F: FnMut(&IndexMap<CaptureKey, Option<String>>) -> String>( - pattern: &str, - mut callback: F, - subject: &str, - limit: i64, - count: Option<&mut usize>, - flags: i64, -) -> Option<String> { - let re = shirabe_php_shim::compile_php_pattern(pattern).ok()?; - let unmatched_as_null = flags & PREG_UNMATCHED_AS_NULL != 0; - let names: Vec<Option<&str>> = re.capture_names().collect(); - let limit = if limit < 0 { - usize::MAX - } else { - limit as usize - }; - - let mut out: Vec<u8> = Vec::new(); - let mut last = 0usize; - let mut n = 0usize; - for caps in re.captures_iter(subject) { - if n >= limit { - break; - } - let m = caps.get(0).unwrap(); - out.extend_from_slice(&subject.as_bytes()[last..m.start()]); - let map = single_match_map(&caps, &names, unmatched_as_null); - out.extend_from_slice(callback(&map).as_bytes()); - last = m.end(); - n += 1; - } - out.extend_from_slice(&subject.as_bytes()[last..]); - - if let Some(count) = count { - *count = n; - } - Some(String::from_utf8_lossy(&out).into_owned()) -} - -pub fn preg_split(pattern: &str, subject: &str, limit: i64, flags: i64) -> Option<Vec<String>> { - let re = shirabe_php_shim::compile_php_pattern(pattern).ok()?; - let no_empty = flags & PREG_SPLIT_NO_EMPTY != 0; - let delim_capture = flags & PREG_SPLIT_DELIM_CAPTURE != 0; - // `limit` counts the resulting pieces; a non-positive value means no limit. - let max_delims = if limit > 0 { - (limit as usize).saturating_sub(1) - } else { - usize::MAX - }; - - let mut result: Vec<String> = Vec::new(); - let mut push = |s: &str, result: &mut Vec<String>| { - if !(no_empty && s.is_empty()) { - result.push(s.to_string()); - } - }; - - let mut last = 0usize; - let mut delims = 0usize; - for caps in re.captures_iter(subject) { - if delims >= max_delims { - break; - } - let m = caps.get(0).unwrap(); - push(&subject[last..m.start()], &mut result); - if delim_capture { - // Mirror preg_match: trailing unmatched groups are dropped, interior - // unmatched groups are emitted as "". - if let Some(last_g) = (1..caps.len()).rev().find(|&g| caps.get(g).is_some()) { - for g in 1..=last_g { - push(caps.get(g).map(|x| x.as_str()).unwrap_or(""), &mut result); - } - } - } - last = m.end(); - delims += 1; - } - push(&subject[last..], &mut result); - - Some(result) -} - -pub fn preg_grep(pattern: &str, array: &[&str], flags: i64) -> Option<Vec<String>> { - let re = shirabe_php_shim::compile_php_pattern(pattern).ok()?; - let invert = flags & PREG_GREP_INVERT != 0; - Some( - array - .iter() - .filter(|s| re.is_match(s) != invert) - .map(|s| s.to_string()) - .collect(), - ) -} - -// Builds a single match's `$matches` map with both named and numbered keys -// (the named key precedes its number). With PREG_UNMATCHED_AS_NULL, every group -// is present and non-participating ones are None; otherwise classic semantics -// apply: trailing unmatched groups are dropped and interior ones become "". -fn single_match_map( - caps: ®ex::Captures, - names: &[Option<&str>], - unmatched_as_null: bool, -) -> IndexMap<CaptureKey, Option<String>> { - let mut out = IndexMap::new(); - let group_count = caps.len(); - let last_participating = (0..group_count).rev().find(|&i| caps.get(i).is_some()); - - for i in 0..group_count { - let m = caps.get(i); - if !unmatched_as_null && m.is_none() { - if let Some(last) = last_participating { - if i > last { - break; - } - } - } - let value = if unmatched_as_null { - m.map(|m| m.as_str().to_string()) - } else { - Some(m.map(|m| m.as_str().to_string()).unwrap_or_default()) - }; - if let Some(Some(name)) = names.get(i) { - out.insert(CaptureKey::ByName((*name).to_string()), value.clone()); - } - out.insert(CaptureKey::ByIndex(i), value); - } - out -} - -// Expands a PHP preg replacement template against `caps`, appending bytes to -// `out`. Backreferences are written as `$1`, `${1}`, `\1` or `\\1`; a literal -// `$` or `\` not forming a reference is emitted verbatim. Out-of-range or -// non-participating groups expand to nothing. -fn expand_php_replacement(template: &str, caps: ®ex::Captures, out: &mut Vec<u8>) { - let bytes = template.as_bytes(); - let mut i = 0; - while i < bytes.len() { - match bytes[i] { - b'\\' if i + 1 < bytes.len() && bytes[i + 1].is_ascii_digit() => { - let (group, consumed) = replacement_group(&bytes[i + 1..]); - if let Some(m) = caps.get(group) { - out.extend_from_slice(m.as_str().as_bytes()); - } - i += 1 + consumed; - } - b'\\' if i + 1 < bytes.len() && bytes[i + 1] == b'\\' => { - out.push(b'\\'); - i += 2; - } - b'$' if i + 1 < bytes.len() && bytes[i + 1] == b'{' => { - let rest = &bytes[i + 2..]; - match rest.iter().position(|&b| b == b'}') { - Some(c) if c > 0 && rest[..c].iter().all(|b| b.is_ascii_digit()) => { - let group: usize = - std::str::from_utf8(&rest[..c]).unwrap().parse().unwrap(); - if let Some(m) = caps.get(group) { - out.extend_from_slice(m.as_str().as_bytes()); - } - i += 2 + c + 1; - } - _ => { - out.push(b'$'); - i += 1; - } - } - } - b'$' if i + 1 < bytes.len() && bytes[i + 1].is_ascii_digit() => { - let (group, consumed) = replacement_group(&bytes[i + 1..]); - if let Some(m) = caps.get(group) { - out.extend_from_slice(m.as_str().as_bytes()); - } - i += 1 + consumed; - } - b => { - out.push(b); - i += 1; - } - } - } -} - -// Reads up to two leading ASCII digits as a PHP backreference group number. -fn replacement_group(bytes: &[u8]) -> (usize, usize) { - let mut group = 0usize; - let mut consumed = 0usize; - while consumed < 2 && consumed < bytes.len() && bytes[consumed].is_ascii_digit() { - group = group * 10 + (bytes[consumed] - b'0') as usize; - consumed += 1; - } - (group, consumed) -} - /// Panics if a pattern is invalid instead of throwing a PcreException. /// TODO: takes regex::Error and shows its message fn invalid_regex() -> ! { |
