aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe-external-packages/src/composer
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-06-14 05:06:27 +0900
committernsfisis <nsfisis@gmail.com>2026-06-14 05:08:54 +0900
commitef9118c788c1cbb22ca7721b6a9e40c2bf2fe243 (patch)
treea955d0cf277cee8adcfeea8f35c672f0a35afddf /crates/shirabe-external-packages/src/composer
parent4116323e9d55745c17b2d8008affa3394b726f6b (diff)
downloadphp-shirabe-ef9118c788c1cbb22ca7721b6a9e40c2bf2fe243.tar.gz
php-shirabe-ef9118c788c1cbb22ca7721b6a9e40c2bf2fe243.tar.zst
php-shirabe-ef9118c788c1cbb22ca7721b6a9e40c2bf2fe243.zip
feat(pcre): implement preg_* functions
Replace the todo!() stubs backing the Preg class and the standalone preg_* shim helpers with regex-crate implementations.
Diffstat (limited to 'crates/shirabe-external-packages/src/composer')
-rw-r--r--crates/shirabe-external-packages/src/composer/pcre/preg.rs347
1 files changed, 304 insertions, 43 deletions
diff --git a/crates/shirabe-external-packages/src/composer/pcre/preg.rs b/crates/shirabe-external-packages/src/composer/pcre/preg.rs
index f14cb03..14be7a2 100644
--- a/crates/shirabe-external-packages/src/composer/pcre/preg.rs
+++ b/crates/shirabe-external-packages/src/composer/pcre/preg.rs
@@ -388,73 +388,334 @@ pub enum CaptureKey {
ByName(String),
}
-pub fn preg_last_error() -> i64 {
- todo!()
-}
-
-pub fn preg_last_error_msg() -> String {
- todo!()
-}
-
// 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,
+ pattern: &str,
+ subject: &str,
+ matches: Option<&mut IndexMap<CaptureKey, Option<String>>>,
+ flags: i64,
+ offset: usize,
) -> Option<i64> {
- todo!()
+ 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,
+ pattern: &str,
+ subject: &str,
+ matches: Option<&mut IndexMap<CaptureKey, Vec<Option<String>>>>,
+ flags: i64,
+ offset: usize,
) -> Option<i64> {
- todo!()
+ 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,
+ pattern: &str,
+ subject: &str,
+ matches: Option<&mut IndexMap<CaptureKey, Vec<(Option<String>, i64)>>>,
+ flags: i64,
+ offset: usize,
) -> Option<i64> {
- todo!()
+ 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>,
+ pattern: &str,
+ replacement: &str,
+ subject: &str,
+ limit: i64,
+ count: Option<&mut usize>,
) -> Option<String> {
- todo!()
+ 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,
- _callback: F,
- _subject: &str,
- _limit: i64,
- _count: Option<&mut usize>,
- _flags: i64,
+ pattern: &str,
+ mut callback: F,
+ subject: &str,
+ limit: i64,
+ count: Option<&mut usize>,
+ flags: i64,
) -> Option<String> {
- todo!()
+ 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_split(_pattern: &str, _subject: &str, _limit: i64, _flags: i64) -> Option<Vec<String>> {
- todo!()
+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(),
+ )
}
-pub fn preg_grep(_pattern: &str, _array: &[&str], _flags: i64) -> Option<Vec<String>> {
- todo!()
+// 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: &regex::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: &regex::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.