aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe-php-shim
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-php-shim
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-php-shim')
-rw-r--r--crates/shirabe-php-shim/src/lib.rs261
1 files changed, 227 insertions, 34 deletions
diff --git a/crates/shirabe-php-shim/src/lib.rs b/crates/shirabe-php-shim/src/lib.rs
index 4a0a3d9..00973ac 100644
--- a/crates/shirabe-php-shim/src/lib.rs
+++ b/crates/shirabe-php-shim/src/lib.rs
@@ -1041,24 +1041,57 @@ pub fn json_encode<T: serde::Serialize + ?Sized>(_value: &T) -> Option<String> {
todo!()
}
-pub fn preg_quote(_str: &str, _delimiter: Option<char>) -> String {
- todo!()
+pub fn preg_quote(str: &str, delimiter: Option<char>) -> String {
+ const SPECIAL: &str = ".\\+*?[^]$(){}=!<>|:-#";
+ let mut out = String::new();
+ for c in str.chars() {
+ if c == '\0' {
+ out.push_str("\\000");
+ } else if SPECIAL.contains(c) || Some(c) == delimiter {
+ out.push('\\');
+ out.push(c);
+ } else {
+ out.push(c);
+ }
+ }
+ out
}
// Returns 1 on match, 0 on no match; populates matches[0]=full match, matches[1..]=captures.
// Optional groups that did not participate in the match are stored as None.
-pub fn preg_match(_pattern: &str, _subject: &str, _matches: &mut Vec<Option<String>>) -> i64 {
- todo!()
+pub fn preg_match(pattern: &str, subject: &str, matches: &mut Vec<Option<String>>) -> i64 {
+ let re = compile_php_pattern(pattern).unwrap_or_else(|e| panic!("invalid regex: {e}"));
+ matches.clear();
+ match re.captures(subject) {
+ Some(caps) => {
+ for g in 0..caps.len() {
+ matches.push(caps.get(g).map(|m| m.as_str().to_string()));
+ }
+ 1
+ }
+ None => 0,
+ }
}
// Returns Some(result) on success, None on error.
-pub fn preg_replace(_pattern: &str, _replacement: &str, _subject: &str) -> Option<String> {
- todo!()
+pub fn preg_replace(pattern: &str, replacement: &str, subject: &str) -> Option<String> {
+ let re = compile_php_pattern(pattern).ok()?;
+ let mut out: Vec<u8> = Vec::new();
+ let mut last = 0;
+ for caps in re.captures_iter(subject) {
+ let m = caps.get(0).unwrap();
+ out.extend_from_slice(&subject.as_bytes()[last..m.start()]);
+ php_replacement_expand(replacement, &caps, &mut out);
+ last = m.end();
+ }
+ out.extend_from_slice(&subject.as_bytes()[last..]);
+ Some(String::from_utf8_lossy(&out).into_owned())
}
// Returns Some(parts) on success, None on error.
-pub fn preg_split(_pattern: &str, _subject: &str) -> Option<Vec<String>> {
- todo!()
+pub fn preg_split(pattern: &str, subject: &str) -> Option<Vec<String>> {
+ let re = compile_php_pattern(pattern).ok()?;
+ Some(php_split_impl(&re, subject))
}
pub fn dirname(_path: &str) -> String {
@@ -2904,40 +2937,92 @@ pub fn exit(status: i64) -> ! {
std::process::exit(status as i32);
}
-pub fn preg_match_all(_pattern: &str, _subject: &str) -> Vec<Vec<String>> {
- todo!()
+// PREG_PATTERN_ORDER: the outer vec is indexed by capture group, the inner by
+// match occurrence. Non-participating groups are reported as "".
+pub fn preg_match_all(pattern: &str, subject: &str) -> Vec<Vec<String>> {
+ let re = compile_php_pattern(pattern).unwrap_or_else(|e| panic!("invalid regex: {e}"));
+ let group_count = re.captures_len();
+ let mut groups: Vec<Vec<String>> = vec![Vec::new(); group_count];
+ for caps in re.captures_iter(subject) {
+ for g in 0..group_count {
+ groups[g].push(
+ caps.get(g)
+ .map(|m| m.as_str().to_string())
+ .unwrap_or_default(),
+ );
+ }
+ }
+ groups
}
pub fn preg_match_all_simple(
- _pattern: &str,
- _subject: &str,
- _matches: &mut Vec<Vec<String>>,
+ pattern: &str,
+ subject: &str,
+ matches: &mut Vec<Vec<String>>,
) -> anyhow::Result<i64> {
- todo!()
+ let re = compile_php_pattern(pattern)?;
+ let group_count = re.captures_len();
+ let mut groups: Vec<Vec<String>> = vec![Vec::new(); group_count];
+ let mut count = 0i64;
+ for caps in re.captures_iter(subject) {
+ count += 1;
+ for g in 0..group_count {
+ groups[g].push(
+ caps.get(g)
+ .map(|m| m.as_str().to_string())
+ .unwrap_or_default(),
+ );
+ }
+ }
+ *matches = groups;
+ Ok(count)
}
+// PREG_SET_ORDER: the outer vec is indexed by match occurrence, the inner by
+// capture group (a classic `$matches` row).
pub fn preg_match_all_set_order(
- _pattern: &str,
- _subject: &str,
- _matches: &mut Vec<Vec<String>>,
+ pattern: &str,
+ subject: &str,
+ matches: &mut Vec<Vec<String>>,
) -> anyhow::Result<i64> {
- todo!()
+ let re = compile_php_pattern(pattern)?;
+ let mut rows: Vec<Vec<String>> = Vec::new();
+ for caps in re.captures_iter(subject) {
+ rows.push(php_match_row(&caps));
+ }
+ let count = rows.len() as i64;
+ *matches = rows;
+ Ok(count)
}
pub fn preg_match_offset(
- _pattern: &str,
- _subject: &str,
- _matches: &mut Vec<String>,
+ pattern: &str,
+ subject: &str,
+ matches: &mut Vec<String>,
_flags: i64,
- _offset: i64,
+ offset: i64,
) -> bool {
- todo!()
+ let re = compile_php_pattern(pattern).unwrap_or_else(|e| panic!("invalid regex: {e}"));
+ match re.captures_at(subject, offset as usize) {
+ Some(caps) => {
+ *matches = php_match_row(&caps);
+ true
+ }
+ None => {
+ matches.clear();
+ false
+ }
+ }
}
-pub fn preg_match_groups(_pattern: &str, _subject: &str) -> Option<Vec<String>> {
- todo!()
+pub fn preg_match_groups(pattern: &str, subject: &str) -> Option<Vec<String>> {
+ let re = compile_php_pattern(pattern).ok()?;
+ let caps = re.captures(subject)?;
+ Some(php_match_row(&caps))
}
-pub fn preg_grep(_pattern: &str, _input: &Vec<String>) -> Vec<String> {
- todo!()
+pub fn preg_grep(pattern: &str, input: &Vec<String>) -> Vec<String> {
+ let re = compile_php_pattern(pattern).unwrap_or_else(|e| panic!("invalid regex: {e}"));
+ input.iter().filter(|s| re.is_match(s)).cloned().collect()
}
-pub fn preg_split_chars(_pattern: &str, _subject: &str) -> Vec<String> {
- todo!()
+pub fn preg_split_chars(pattern: &str, subject: &str) -> Vec<String> {
+ let re = compile_php_pattern(pattern).unwrap_or_else(|e| panic!("invalid regex: {e}"));
+ php_split_impl(&re, subject)
}
#[derive(Debug, Default)]
@@ -2955,7 +3040,7 @@ impl PregOffsetCaptureMatches {
// lookaround, backreferences) are not supported by `regex` and must be avoided
// in the caller's pattern.
// TODO(phase-c): replace with a faithful PCRE engine to restore full semantics.
-fn compile_php_pattern(pattern: &str) -> anyhow::Result<regex::Regex> {
+pub fn compile_php_pattern(pattern: &str) -> anyhow::Result<regex::Regex> {
let delimiter = pattern
.chars()
.next()
@@ -2981,6 +3066,100 @@ fn compile_php_pattern(pattern: &str) -> anyhow::Result<regex::Regex> {
Ok(regex::Regex::new(&translated)?)
}
+// 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 php_replacement_expand(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) = php_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) = php_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 php_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)
+}
+
+// PHP `preg_split($pattern, $subject)` with no flags or limit: the text between
+// successive matches, including the leading and trailing pieces (which may be
+// empty). Zero-width matches split between every position.
+fn php_split_impl(re: &regex::Regex, subject: &str) -> Vec<String> {
+ let mut result = Vec::new();
+ let mut last = 0;
+ for caps in re.captures_iter(subject) {
+ let m = caps.get(0).unwrap();
+ result.push(subject[last..m.start()].to_string());
+ last = m.end();
+ }
+ result.push(subject[last..].to_string());
+ result
+}
+
+// Classic preg_match `$matches` row: index 0 is the full match, trailing
+// unmatched groups are truncated and interior unmatched groups become "".
+fn php_match_row(caps: &regex::Captures) -> Vec<String> {
+ let last = (0..caps.len())
+ .rev()
+ .find(|&g| caps.get(g).is_some())
+ .unwrap_or(0);
+ (0..=last)
+ .map(|g| {
+ caps.get(g)
+ .map(|m| m.as_str().to_string())
+ .unwrap_or_default()
+ })
+ .collect()
+}
+
pub fn preg_match_all_offset_capture(
pattern: &str,
subject: &str,
@@ -3008,14 +3187,28 @@ pub fn preg_match_all_offset_capture(
Ok(count)
}
pub fn preg_replace_callback<F>(
- _pattern: &str,
- _callback: F,
- _subject: &str,
+ pattern: &str,
+ mut callback: F,
+ subject: &str,
) -> anyhow::Result<String>
where
F: FnMut(&[Option<String>]) -> anyhow::Result<String>,
{
- todo!()
+ let re = compile_php_pattern(pattern)?;
+ let mut out: Vec<u8> = Vec::new();
+ let mut last = 0;
+ for caps in re.captures_iter(subject) {
+ let m = caps.get(0).unwrap();
+ out.extend_from_slice(&subject.as_bytes()[last..m.start()]);
+ let groups: Vec<Option<String>> = (0..caps.len())
+ .map(|g| caps.get(g).map(|x| x.as_str().to_string()))
+ .collect();
+ let replaced = callback(&groups)?;
+ out.extend_from_slice(replaced.as_bytes());
+ last = m.end();
+ }
+ out.extend_from_slice(&subject.as_bytes()[last..]);
+ Ok(String::from_utf8_lossy(&out).into_owned())
}
pub fn is_resource_value(_resource: &PhpResource) -> bool {