diff options
Diffstat (limited to 'crates/shirabe-php-shim/src/lib.rs')
| -rw-r--r-- | crates/shirabe-php-shim/src/lib.rs | 332 |
1 files changed, 4 insertions, 328 deletions
diff --git a/crates/shirabe-php-shim/src/lib.rs b/crates/shirabe-php-shim/src/lib.rs index 00973ac..40b0060 100644 --- a/crates/shirabe-php-shim/src/lib.rs +++ b/crates/shirabe-php-shim/src/lib.rs @@ -1,3 +1,7 @@ +mod preg; + +pub use preg::*; + use indexmap::IndexMap; #[derive(Debug, Clone, Default)] @@ -1041,59 +1045,6 @@ pub fn json_encode<T: serde::Serialize + ?Sized>(_value: &T) -> Option<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 { - 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> { - 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>> { - let re = compile_php_pattern(pattern).ok()?; - Some(php_split_impl(&re, subject)) -} - pub fn dirname(_path: &str) -> String { todo!() } @@ -2738,7 +2689,6 @@ pub const OPENSSL_VERSION_NUMBER: i64 = 0; pub const OPENSSL_VERSION_TEXT: &str = ""; pub const PHP_BINARY: &str = ""; pub const PHP_WINDOWS_VERSION_BUILD: i64 = 0; -pub const PREG_BACKTRACK_LIMIT_ERROR: i64 = 2; #[derive(Debug, Clone)] pub struct ArrayObject { @@ -2937,280 +2887,6 @@ pub fn exit(status: i64) -> ! { std::process::exit(status as i32); } -// 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>>, -) -> anyhow::Result<i64> { - 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>>, -) -> anyhow::Result<i64> { - 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>, - _flags: i64, - offset: i64, -) -> bool { - 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>> { - 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> { - 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> { - let re = compile_php_pattern(pattern).unwrap_or_else(|e| panic!("invalid regex: {e}")); - php_split_impl(&re, subject) -} - -#[derive(Debug, Default)] -pub struct PregOffsetCaptureMatches { - groups: Vec<Vec<(String, usize)>>, -} -impl PregOffsetCaptureMatches { - pub fn group(&self, i: usize) -> &[(String, usize)] { - &self.groups[i] - } -} -// Translates a PHP PCRE pattern (delimiters + trailing modifiers) into a regex -// the `regex` crate can compile. Only delimiter stripping and the i/x/s/m -// modifiers are handled; PCRE-only constructs (possessive quantifiers, -// 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. -pub fn compile_php_pattern(pattern: &str) -> anyhow::Result<regex::Regex> { - let delimiter = pattern - .chars() - .next() - .ok_or_else(|| anyhow::anyhow!("empty regex pattern"))?; - let end = pattern - .rfind(delimiter) - .filter(|&i| i > 0) - .ok_or_else(|| anyhow::anyhow!("unterminated regex pattern: {pattern}"))?; - let inner = &pattern[delimiter.len_utf8()..end]; - let modifiers = &pattern[end + delimiter.len_utf8()..]; - - let flags: String = modifiers - .chars() - .filter(|c| matches!(c, 'i' | 'x' | 's' | 'm')) - .collect(); - - let translated = if flags.is_empty() { - inner.to_string() - } else { - format!("(?{flags}){inner}") - }; - - 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: ®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) = 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: ®ex::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: ®ex::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, - matches: &mut PregOffsetCaptureMatches, -) -> anyhow::Result<i64> { - let re = compile_php_pattern(pattern)?; - let group_count = re.captures_len(); - matches.groups = vec![Vec::new(); group_count]; - - let mut count = 0; - for caps in re.captures_iter(subject) { - count += 1; - for g in 0..group_count { - // PHP stores ["", -1] for non-participating groups under - // PREG_OFFSET_CAPTURE; the unsigned offset here approximates -1 as 0, - // which callers must not rely on for absent groups. - let entry = caps - .get(g) - .map(|m| (m.as_str().to_string(), m.start())) - .unwrap_or_else(|| (String::new(), 0)); - matches.groups[g].push(entry); - } - } - - Ok(count) -} -pub fn preg_replace_callback<F>( - pattern: &str, - mut callback: F, - subject: &str, -) -> anyhow::Result<String> -where - F: FnMut(&[Option<String>]) -> anyhow::Result<String>, -{ - 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 { true } |
