diff options
Diffstat (limited to 'crates/shirabe-external-packages/src/composer/pcre/preg.rs')
| -rw-r--r-- | crates/shirabe-external-packages/src/composer/pcre/preg.rs | 347 |
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: ®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. |
