From 1378a4818fe5933867328fd4e5972ff9db240dd1 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Sun, 14 Jun 2026 03:00:27 +0900 Subject: feat(pcre): implement Preg class Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/composer/pcre/mod.rs | 2 + .../src/composer/pcre/pcre_exception.rs | 36 + .../src/composer/pcre/preg.rs | 992 ++++++++++++++------- .../pcre/unexpected_null_match_exception.rs | 29 + 4 files changed, 754 insertions(+), 305 deletions(-) create mode 100644 crates/shirabe-external-packages/src/composer/pcre/pcre_exception.rs create mode 100644 crates/shirabe-external-packages/src/composer/pcre/unexpected_null_match_exception.rs (limited to 'crates') diff --git a/crates/shirabe-external-packages/src/composer/pcre/mod.rs b/crates/shirabe-external-packages/src/composer/pcre/mod.rs index 20c37d0..41def3b 100644 --- a/crates/shirabe-external-packages/src/composer/pcre/mod.rs +++ b/crates/shirabe-external-packages/src/composer/pcre/mod.rs @@ -1,3 +1,5 @@ +pub mod pcre_exception; pub mod preg; +pub mod unexpected_null_match_exception; pub use preg::*; diff --git a/crates/shirabe-external-packages/src/composer/pcre/pcre_exception.rs b/crates/shirabe-external-packages/src/composer/pcre/pcre_exception.rs new file mode 100644 index 0000000..4ebe2c3 --- /dev/null +++ b/crates/shirabe-external-packages/src/composer/pcre/pcre_exception.rs @@ -0,0 +1,36 @@ +//! ref: composer/vendor/composer/pcre/src/PcreException.php + +use super::preg::{preg_last_error, preg_last_error_msg}; + +#[derive(Debug)] +pub struct PcreException(pub shirabe_php_shim::RuntimeException); + +impl PcreException { + pub fn from_function(function: &str, pattern: &str) -> PcreException { + let code = preg_last_error(); + + PcreException(shirabe_php_shim::RuntimeException { + message: format!( + "{}(): failed executing \"{}\": {}", + function, + pattern, + Self::pcre_last_error_message(code) + ), + code, + }) + } + + // Modern PHP always provides preg_last_error_msg(), so the legacy fallbacks + // that scanned the pcre constants are not reproduced. + fn pcre_last_error_message(_code: i64) -> String { + preg_last_error_msg() + } +} + +impl std::fmt::Display for PcreException { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl std::error::Error for PcreException {} diff --git a/crates/shirabe-external-packages/src/composer/pcre/preg.rs b/crates/shirabe-external-packages/src/composer/pcre/preg.rs index c918b80..fba8c0b 100644 --- a/crates/shirabe-external-packages/src/composer/pcre/preg.rs +++ b/crates/shirabe-external-packages/src/composer/pcre/preg.rs @@ -1,543 +1,925 @@ //! ref: composer/vendor/composer/pcre/src/Preg.php +use super::pcre_exception::PcreException; +use super::unexpected_null_match_exception::UnexpectedNullMatchException; +use indexmap::IndexMap; + +pub const PREG_PATTERN_ORDER: i64 = 1; +pub const PREG_SET_ORDER: i64 = 2; +pub const PREG_OFFSET_CAPTURE: i64 = 256; +pub const PREG_UNMATCHED_AS_NULL: i64 = 512; +pub const PREG_SPLIT_NO_EMPTY: i64 = 1; +pub const PREG_SPLIT_DELIM_CAPTURE: i64 = 2; +pub const PREG_SPLIT_OFFSET_CAPTURE: i64 = 4; +pub const PREG_GREP_INVERT: i64 = 1; + #[derive(Debug)] pub struct Preg; impl Preg { - pub fn r#match(_pattern: &str, _subject: &str) -> anyhow::Result { - todo!() + const ARRAY_MSG: &'static str = + "$subject as an array is not supported. You can use 'foreach' instead."; + const INVALID_TYPE_MSG: &'static str = "$subject must be a string, %s given."; + + pub fn r#match(pattern: &str, subject: &str) -> anyhow::Result { + Self::match5(pattern, subject, None, 0, 0) } pub fn match3( - _pattern: &str, - _subject: &str, - _matches: Option<&mut indexmap::IndexMap>, + pattern: &str, + subject: &str, + matches: Option<&mut IndexMap>, ) -> anyhow::Result { - todo!() + Self::match5(pattern, subject, matches, 0, 0) } pub fn match4( - _pattern: &str, - _subject: &str, - _matches: Option<&mut indexmap::IndexMap>, - _flags: i64, + pattern: &str, + subject: &str, + matches: Option<&mut IndexMap>, + flags: i64, ) -> anyhow::Result { - todo!() + Self::match5(pattern, subject, matches, flags, 0) } pub fn match5( - _pattern: &str, - _subject: &str, - _matches: Option<&mut indexmap::IndexMap>, - _flags: i64, - _offset: usize, + pattern: &str, + subject: &str, + matches: Option<&mut IndexMap>, + flags: i64, + offset: usize, ) -> anyhow::Result { - todo!() + Self::check_offset_capture(flags, "matchWithOffsets"); + + let mut internal: IndexMap> = IndexMap::new(); + let result = preg_match( + pattern, + subject, + Some(&mut internal), + flags | PREG_UNMATCHED_AS_NULL, + offset, + ) + .ok_or_else(|| PcreException::from_function("preg_match", pattern))?; + + if let Some(out) = matches { + *out = drop_null_matches(internal); + } + + Ok(result == 1) } - pub fn match_strict_groups(_pattern: &str, _subject: &str) -> anyhow::Result { - todo!() + pub fn match_strict_groups(pattern: &str, subject: &str) -> anyhow::Result { + Self::match_strict_groups5(pattern, subject, None, 0, 0) } pub fn match_strict_groups3( - _pattern: &str, - _subject: &str, - _matches: Option<&mut indexmap::IndexMap>, + pattern: &str, + subject: &str, + matches: Option<&mut IndexMap>, ) -> anyhow::Result { - todo!() + Self::match_strict_groups5(pattern, subject, matches, 0, 0) } pub fn match_strict_groups4( - _pattern: &str, - _subject: &str, - _matches: Option<&mut indexmap::IndexMap>, - _flags: i64, + pattern: &str, + subject: &str, + matches: Option<&mut IndexMap>, + flags: i64, ) -> anyhow::Result { - todo!() + Self::match_strict_groups5(pattern, subject, matches, flags, 0) } pub fn match_strict_groups5( - _pattern: &str, - _subject: &str, - _matches: Option<&mut indexmap::IndexMap>, - _flags: i64, - _offset: usize, + pattern: &str, + subject: &str, + matches: Option<&mut IndexMap>, + flags: i64, + offset: usize, ) -> anyhow::Result { - todo!() + Self::check_offset_capture(flags, "matchWithOffsets"); + + let mut internal: IndexMap> = IndexMap::new(); + let result = preg_match( + pattern, + subject, + Some(&mut internal), + flags | PREG_UNMATCHED_AS_NULL, + offset, + ) + .ok_or_else(|| PcreException::from_function("preg_match", pattern))?; + + let enforced = Self::enforce_non_null_matches(pattern, internal, "match")?; + if let Some(out) = matches { + *out = enforced; + } + + Ok(result == 1) } - pub fn match_with_offsets(_pattern: &str, _subject: &str) -> anyhow::Result { - todo!() + pub fn match_with_offsets(pattern: &str, subject: &str) -> anyhow::Result { + Self::match_with_offsets5(pattern, subject, None, 0, 0) } pub fn match_with_offsets3( - _pattern: &str, - _subject: &str, - _matches: Option<&mut indexmap::IndexMap>, + pattern: &str, + subject: &str, + matches: Option<&mut IndexMap>, ) -> anyhow::Result { - todo!() + Self::match_with_offsets5(pattern, subject, matches, 0, 0) } pub fn match_with_offsets4( - _pattern: &str, - _subject: &str, - _matches: Option<&mut indexmap::IndexMap>, - _flags: i64, + pattern: &str, + subject: &str, + matches: Option<&mut IndexMap>, + flags: i64, ) -> anyhow::Result { - todo!() + Self::match_with_offsets5(pattern, subject, matches, flags, 0) } pub fn match_with_offsets5( - _pattern: &str, - _subject: &str, - _matches: Option<&mut indexmap::IndexMap>, - _flags: i64, - _offset: usize, + pattern: &str, + subject: &str, + matches: Option<&mut IndexMap>, + flags: i64, + offset: usize, ) -> anyhow::Result { - todo!() + let mut internal: IndexMap, i64)> = IndexMap::new(); + let result = preg_match_offset_capture( + pattern, + subject, + Some(&mut internal), + flags | PREG_UNMATCHED_AS_NULL | PREG_OFFSET_CAPTURE, + offset, + ) + .ok_or_else(|| PcreException::from_function("preg_match", pattern))?; + + if let Some(out) = matches { + *out = drop_null_offset_matches(internal); + } + + Ok(result == 1) } - pub fn match_all(_pattern: &str, _subject: &str) -> anyhow::Result { - todo!() + pub fn match_all(pattern: &str, subject: &str) -> anyhow::Result { + Self::match_all5(pattern, subject, None, 0, 0) } pub fn match_all3( - _pattern: &str, - _subject: &str, - _matches: Option<&mut indexmap::IndexMap>>, + pattern: &str, + subject: &str, + matches: Option<&mut IndexMap>>, ) -> anyhow::Result { - todo!() + Self::match_all5(pattern, subject, matches, 0, 0) } pub fn match_all4( - _pattern: &str, - _subject: &str, - _matches: Option<&mut indexmap::IndexMap>>, - _flags: i64, + pattern: &str, + subject: &str, + matches: Option<&mut IndexMap>>, + flags: i64, ) -> anyhow::Result { - todo!() + Self::match_all5(pattern, subject, matches, flags, 0) } pub fn match_all5( - _pattern: &str, - _subject: &str, - _matches: Option<&mut indexmap::IndexMap>>, - _flags: i64, - _offset: usize, + pattern: &str, + subject: &str, + matches: Option<&mut IndexMap>>, + flags: i64, + offset: usize, ) -> anyhow::Result { - todo!() + Self::check_offset_capture(flags, "matchAllWithOffsets"); + Self::check_set_order(flags); + + let mut internal: IndexMap>> = IndexMap::new(); + let result = preg_match_all( + pattern, + subject, + Some(&mut internal), + flags | PREG_UNMATCHED_AS_NULL, + offset, + ) + .ok_or_else(|| PcreException::from_function("preg_match_all", pattern))?; + + if let Some(out) = matches { + *out = null_to_empty_match_all(internal); + } + + Ok(result as usize) } - pub fn match_all_strict_groups(_pattern: &str, _subject: &str) -> anyhow::Result { - todo!() + pub fn match_all_strict_groups(pattern: &str, subject: &str) -> anyhow::Result { + Self::match_all_strict_groups5(pattern, subject, None, 0, 0) } pub fn match_all_strict_groups3( - _pattern: &str, - _subject: &str, - _matches: Option<&mut indexmap::IndexMap>>, + pattern: &str, + subject: &str, + matches: Option<&mut IndexMap>>, ) -> anyhow::Result { - todo!() + Self::match_all_strict_groups5(pattern, subject, matches, 0, 0) } pub fn match_all_strict_groups4( - _pattern: &str, - _subject: &str, - _matches: Option<&mut indexmap::IndexMap>>, - _flags: i64, + pattern: &str, + subject: &str, + matches: Option<&mut IndexMap>>, + flags: i64, ) -> anyhow::Result { - todo!() + Self::match_all_strict_groups5(pattern, subject, matches, flags, 0) } pub fn match_all_strict_groups5( - _pattern: &str, - _subject: &str, - _matches: Option<&mut indexmap::IndexMap>>, - _flags: i64, - _offset: usize, + pattern: &str, + subject: &str, + matches: Option<&mut IndexMap>>, + flags: i64, + offset: usize, ) -> anyhow::Result { - todo!() + Self::check_offset_capture(flags, "matchAllWithOffsets"); + Self::check_set_order(flags); + + let mut internal: IndexMap>> = IndexMap::new(); + let result = preg_match_all( + pattern, + subject, + Some(&mut internal), + flags | PREG_UNMATCHED_AS_NULL, + offset, + ) + .ok_or_else(|| PcreException::from_function("preg_match_all", pattern))?; + + let enforced = Self::enforce_non_null_match_all(pattern, internal, "matchAll")?; + if let Some(out) = matches { + *out = enforced; + } + + Ok(result as usize) } - pub fn match_all_with_offsets(_pattern: &str, _subject: &str) -> anyhow::Result { - todo!() + pub fn match_all_with_offsets(pattern: &str, subject: &str) -> anyhow::Result { + Self::match_all_with_offsets5(pattern, subject, None, 0, 0) } pub fn match_all_with_offsets3( - _pattern: &str, - _subject: &str, - _matches: Option<&mut indexmap::IndexMap>>, + pattern: &str, + subject: &str, + matches: Option<&mut IndexMap>>, ) -> anyhow::Result { - todo!() + Self::match_all_with_offsets5(pattern, subject, matches, 0, 0) } pub fn match_all_with_offsets4( - _pattern: &str, - _subject: &str, - _matches: Option<&mut indexmap::IndexMap>>, - _flags: i64, + pattern: &str, + subject: &str, + matches: Option<&mut IndexMap>>, + flags: i64, ) -> anyhow::Result { - todo!() + Self::match_all_with_offsets5(pattern, subject, matches, flags, 0) } pub fn match_all_with_offsets5( - _pattern: &str, - _subject: &str, - _matches: Option<&mut indexmap::IndexMap>>, - _flags: i64, - _offset: usize, + pattern: &str, + subject: &str, + matches: Option<&mut IndexMap>>, + flags: i64, + offset: usize, ) -> anyhow::Result { - todo!() - } + Self::check_set_order(flags); - pub fn replace(_pattern: &str, _replacement: &str, _subject: &str) -> anyhow::Result { - todo!() - } + let mut internal: IndexMap, i64)>> = IndexMap::new(); + let result = preg_match_all_offset_capture( + pattern, + subject, + Some(&mut internal), + flags | PREG_UNMATCHED_AS_NULL | PREG_OFFSET_CAPTURE, + offset, + ) + .ok_or_else(|| PcreException::from_function("preg_match_all", pattern))?; - pub fn replace4( - _pattern: &str, - _replacement: &str, - _subject: &str, - _limit: i64, - ) -> anyhow::Result { - todo!() - } + if let Some(out) = matches { + *out = null_to_empty_offset_match_all(internal); + } - pub fn replace5( - _pattern: &str, - _replacement: &str, - _subject: &str, - _limit: i64, - _count: &mut usize, - ) -> anyhow::Result { - todo!() + Ok(result as usize) } - pub fn replace_callback) -> String>( - _pattern: &str, - _replacement: F, - _subject: &str, - ) -> anyhow::Result { - todo!() + pub fn replace(pattern: &str, replacement: &str, subject: &str) -> anyhow::Result { + Self::replace_impl(pattern, replacement, subject, -1, None) } - pub fn replace_callback4) -> String>( - _pattern: &str, - _replacement: F, - _subject: &str, - _limit: i64, + pub fn replace4( + pattern: &str, + replacement: &str, + subject: &str, + limit: i64, ) -> anyhow::Result { - todo!() + Self::replace_impl(pattern, replacement, subject, limit, None) } - pub fn replace_callback5) -> String>( - _pattern: &str, - _replacement: F, - _subject: &str, - _limit: i64, - _count: &mut usize, + pub fn replace5( + pattern: &str, + replacement: &str, + subject: &str, + limit: i64, + count: &mut usize, ) -> anyhow::Result { - todo!() + Self::replace_impl(pattern, replacement, subject, limit, Some(count)) } - pub fn replace_callback6) -> String>( - _pattern: &str, - _replacement: F, - _subject: &str, - _limit: i64, - _count: Option<&mut usize>, - _flags: i64, + fn replace_impl( + pattern: &str, + replacement: &str, + subject: &str, + limit: i64, + count: Option<&mut usize>, ) -> anyhow::Result { - todo!() + // `$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) + .ok_or_else(|| PcreException::from_function("preg_replace", pattern).into()) } - pub fn replace_callback_array( - _pattern: &indexmap::IndexMap, - _subject: &str, + pub fn replace_callback) -> String>( + pattern: &str, + replacement: F, + subject: &str, ) -> anyhow::Result { - todo!() + Self::replace_callback6(pattern, replacement, subject, -1, None, 0) } - pub fn replace_callback_array3( - _pattern: &indexmap::IndexMap, - _subject: &str, - _limit: i64, + pub fn replace_callback4) -> String>( + pattern: &str, + replacement: F, + subject: &str, + limit: i64, ) -> anyhow::Result { - todo!() + Self::replace_callback6(pattern, replacement, subject, limit, None, 0) } - pub fn replace_callback_array4( - _pattern: &indexmap::IndexMap, - _subject: &str, - _limit: i64, - _count: &mut usize, + pub fn replace_callback5) -> String>( + pattern: &str, + replacement: F, + subject: &str, + limit: i64, + count: &mut usize, ) -> anyhow::Result { - todo!() + Self::replace_callback6(pattern, replacement, subject, limit, Some(count), 0) } - pub fn replace_callback_array5( - _pattern: &indexmap::IndexMap, - _subject: &str, - _limit: i64, - _count: Option<&mut usize>, - _flags: i64, + pub fn replace_callback6) -> String>( + pattern: &str, + mut replacement: F, + subject: &str, + limit: i64, + count: Option<&mut usize>, + flags: i64, ) -> anyhow::Result { - todo!() + let adapter = |internal: &IndexMap>| -> String { + replacement(&drop_null_matches_ref(internal)) + }; + + preg_replace_callback(pattern, adapter, subject, limit, count, flags) + .ok_or_else(|| PcreException::from_function("preg_replace_callback", pattern).into()) } - pub fn split(_pattern: &str, _subject: &str) -> anyhow::Result> { - todo!() + pub fn split(pattern: &str, subject: &str) -> anyhow::Result> { + Self::split4(pattern, subject, -1, 0) } - pub fn split3(_pattern: &str, _subject: &str, _limit: i64) -> anyhow::Result> { - todo!() + pub fn split3(pattern: &str, subject: &str, limit: i64) -> anyhow::Result> { + Self::split4(pattern, subject, limit, 0) } pub fn split4( - _pattern: &str, - _subject: &str, - _limit: i64, - _flags: i64, + pattern: &str, + subject: &str, + limit: i64, + flags: i64, ) -> anyhow::Result> { - todo!() + assert!( + flags & PREG_SPLIT_OFFSET_CAPTURE == 0, + "PREG_SPLIT_OFFSET_CAPTURE is not supported as it changes the type of $matches, use splitWithOffsets() instead" + ); + + preg_split(pattern, subject, limit, flags) + .ok_or_else(|| PcreException::from_function("preg_split", pattern).into()) } pub fn split_with_offsets( - _pattern: &str, - _subject: &str, + pattern: &str, + subject: &str, ) -> anyhow::Result> { - todo!() + Self::split_with_offsets4(pattern, subject, -1, 0) } pub fn split_with_offsets3( - _pattern: &str, - _subject: &str, - _limit: i64, + pattern: &str, + subject: &str, + limit: i64, ) -> anyhow::Result> { - todo!() + Self::split_with_offsets4(pattern, subject, limit, 0) } pub fn split_with_offsets4( - _pattern: &str, - _subject: &str, - _limit: i64, - _flags: i64, + pattern: &str, + subject: &str, + limit: i64, + flags: i64, ) -> anyhow::Result> { - todo!() + preg_split_offset_capture(pattern, subject, limit, flags | PREG_SPLIT_OFFSET_CAPTURE) + .ok_or_else(|| PcreException::from_function("preg_split", pattern).into()) } - pub fn grep(_pattern: &str, _array: &[&str]) -> anyhow::Result> { - todo!() + pub fn grep(pattern: &str, array: &[&str]) -> anyhow::Result> { + Self::grep3(pattern, array, 0) } - pub fn grep3(_pattern: &str, _array: &[&str], _flags: i64) -> anyhow::Result> { - todo!() + pub fn grep3(pattern: &str, array: &[&str], flags: i64) -> anyhow::Result> { + preg_grep(pattern, array, flags) + .ok_or_else(|| PcreException::from_function("preg_grep", pattern).into()) } - pub fn is_match(_pattern: &str, _subject: &str) -> anyhow::Result { - todo!() + pub fn is_match(pattern: &str, subject: &str) -> anyhow::Result { + Self::match5(pattern, subject, None, 0, 0) } pub fn is_match3( - _pattern: &str, - _subject: &str, - _matches: Option<&mut indexmap::IndexMap>, + pattern: &str, + subject: &str, + matches: Option<&mut IndexMap>, ) -> anyhow::Result { - todo!() + Self::match5(pattern, subject, matches, 0, 0) } pub fn is_match4( - _pattern: &str, - _subject: &str, - _matches: Option<&mut indexmap::IndexMap>, - _flags: i64, + pattern: &str, + subject: &str, + matches: Option<&mut IndexMap>, + flags: i64, ) -> anyhow::Result { - todo!() + Self::match5(pattern, subject, matches, flags, 0) } pub fn is_match5( - _pattern: &str, - _subject: &str, - _matches: Option<&mut indexmap::IndexMap>, - _flags: i64, - _offset: usize, + pattern: &str, + subject: &str, + matches: Option<&mut IndexMap>, + flags: i64, + offset: usize, ) -> anyhow::Result { - todo!() + Self::match5(pattern, subject, matches, flags, offset) } pub fn is_match_named( - _pattern: &str, - _subject: &str, - _matches: &mut indexmap::IndexMap, + pattern: &str, + subject: &str, + matches: &mut IndexMap, ) -> anyhow::Result { - todo!() + let mut internal: IndexMap> = IndexMap::new(); + let result = preg_match( + pattern, + subject, + Some(&mut internal), + PREG_UNMATCHED_AS_NULL, + 0, + ) + .ok_or_else(|| PcreException::from_function("preg_match", pattern))?; + + matches.clear(); + for (key, value) in internal { + if let (CaptureKey::ByName(name), Some(value)) = (key, value) { + matches.insert(name, value); + } + } + + Ok(result == 1) } - pub fn is_match_strict_groups(_pattern: &str, _subject: &str) -> anyhow::Result { - todo!() + pub fn is_match_strict_groups(pattern: &str, subject: &str) -> anyhow::Result { + Self::match_strict_groups5(pattern, subject, None, 0, 0) } pub fn is_match_strict_groups3( - _pattern: &str, - _subject: &str, - _matches: Option<&mut indexmap::IndexMap>, + pattern: &str, + subject: &str, + matches: Option<&mut IndexMap>, ) -> anyhow::Result { - todo!() + Self::match_strict_groups5(pattern, subject, matches, 0, 0) } pub fn is_match_strict_groups4( - _pattern: &str, - _subject: &str, - _matches: Option<&mut indexmap::IndexMap>, - _flags: i64, + pattern: &str, + subject: &str, + matches: Option<&mut IndexMap>, + flags: i64, ) -> anyhow::Result { - todo!() + Self::match_strict_groups5(pattern, subject, matches, flags, 0) } pub fn is_match_strict_groups5( - _pattern: &str, - _subject: &str, - _matches: Option<&mut indexmap::IndexMap>, - _flags: i64, - _offset: usize, + pattern: &str, + subject: &str, + matches: Option<&mut IndexMap>, + flags: i64, + offset: usize, ) -> anyhow::Result { - todo!() + Self::match_strict_groups5(pattern, subject, matches, flags, offset) } - pub fn is_match_with_offsets(_pattern: &str, _subject: &str) -> anyhow::Result { - todo!() + pub fn is_match_with_offsets(pattern: &str, subject: &str) -> anyhow::Result { + Self::match_with_offsets5(pattern, subject, None, 0, 0) } pub fn is_match_with_offsets3( - _pattern: &str, - _subject: &str, - _matches: Option<&mut indexmap::IndexMap>, + pattern: &str, + subject: &str, + matches: Option<&mut IndexMap>, ) -> anyhow::Result { - todo!() + Self::match_with_offsets5(pattern, subject, matches, 0, 0) } pub fn is_match_with_offsets4( - _pattern: &str, - _subject: &str, - _matches: Option<&mut indexmap::IndexMap>, - _flags: i64, + pattern: &str, + subject: &str, + matches: Option<&mut IndexMap>, + flags: i64, ) -> anyhow::Result { - todo!() + Self::match_with_offsets5(pattern, subject, matches, flags, 0) } pub fn is_match_with_offsets5( - _pattern: &str, - _subject: &str, - _matches: Option<&mut indexmap::IndexMap>, - _flags: i64, - _offset: usize, + pattern: &str, + subject: &str, + matches: Option<&mut IndexMap>, + flags: i64, + offset: usize, ) -> anyhow::Result { - todo!() + Self::match_with_offsets5(pattern, subject, matches, flags, offset) } - pub fn is_match_all(_pattern: &str, _subject: &str) -> anyhow::Result { - todo!() + pub fn is_match_all(pattern: &str, subject: &str) -> anyhow::Result { + Ok(Self::match_all5(pattern, subject, None, 0, 0)? > 0) } pub fn is_match_with_indexed_captures( - _pattern: &str, - _subject: &str, + pattern: &str, + subject: &str, ) -> anyhow::Result>> { - todo!() + // Classic preg_match semantics (no PREG_UNMATCHED_AS_NULL): trailing + // unmatched groups are truncated, interior unmatched groups become "". + let mut internal: IndexMap> = IndexMap::new(); + let result = preg_match(pattern, subject, Some(&mut internal), 0, 0) + .ok_or_else(|| PcreException::from_function("preg_match", pattern))?; + + if result == 0 { + return Ok(None); + } + + let max_index = internal + .keys() + .filter_map(|key| match key { + CaptureKey::ByIndex(index) => Some(*index), + CaptureKey::ByName(_) => None, + }) + .max() + .unwrap_or(0); + + let mut captures = Vec::with_capacity(max_index + 1); + for index in 0..=max_index { + let value = internal + .get(&CaptureKey::ByIndex(index)) + .and_then(|value| value.clone()) + .unwrap_or_default(); + captures.push(value); + } + + Ok(Some(captures)) } pub fn is_match_all3( - _pattern: &str, - _subject: &str, - _matches: Option<&mut indexmap::IndexMap>>, + pattern: &str, + subject: &str, + matches: Option<&mut IndexMap>>, ) -> anyhow::Result { - todo!() + Ok(Self::match_all5(pattern, subject, matches, 0, 0)? > 0) } pub fn is_match_all4( - _pattern: &str, - _subject: &str, - _matches: Option<&mut indexmap::IndexMap>>, - _flags: i64, + pattern: &str, + subject: &str, + matches: Option<&mut IndexMap>>, + flags: i64, ) -> anyhow::Result { - todo!() + Ok(Self::match_all5(pattern, subject, matches, flags, 0)? > 0) } pub fn is_match_all5( - _pattern: &str, - _subject: &str, - _matches: Option<&mut indexmap::IndexMap>>, - _flags: i64, - _offset: usize, + pattern: &str, + subject: &str, + matches: Option<&mut IndexMap>>, + flags: i64, + offset: usize, ) -> anyhow::Result { - todo!() + Ok(Self::match_all5(pattern, subject, matches, flags, offset)? > 0) } - pub fn is_match_all_strict_groups(_pattern: &str, _subject: &str) -> anyhow::Result { - todo!() + pub fn is_match_all_strict_groups(pattern: &str, subject: &str) -> anyhow::Result { + Ok(Self::match_all_strict_groups5(pattern, subject, None, 0, 0)? > 0) } pub fn is_match_all_strict_groups3( - _pattern: &str, - _subject: &str, - _matches: Option<&mut indexmap::IndexMap>>, + pattern: &str, + subject: &str, + matches: Option<&mut IndexMap>>, ) -> anyhow::Result { - todo!() + Ok(Self::match_all_strict_groups5(pattern, subject, matches, 0, 0)? > 0) } pub fn is_match_all_strict_groups4( - _pattern: &str, - _subject: &str, - _matches: Option<&mut indexmap::IndexMap>>, - _flags: i64, + pattern: &str, + subject: &str, + matches: Option<&mut IndexMap>>, + flags: i64, ) -> anyhow::Result { - todo!() + Ok(Self::match_all_strict_groups5(pattern, subject, matches, flags, 0)? > 0) } pub fn is_match_all_strict_groups5( - _pattern: &str, - _subject: &str, - _matches: Option<&mut indexmap::IndexMap>>, - _flags: i64, - _offset: usize, + pattern: &str, + subject: &str, + matches: Option<&mut IndexMap>>, + flags: i64, + offset: usize, ) -> anyhow::Result { - todo!() + Ok(Self::match_all_strict_groups5(pattern, subject, matches, flags, offset)? > 0) } - pub fn is_match_all_with_offsets(_pattern: &str, _subject: &str) -> anyhow::Result { - todo!() + pub fn is_match_all_with_offsets(pattern: &str, subject: &str) -> anyhow::Result { + Ok(Self::match_all_with_offsets5(pattern, subject, None, 0, 0)? > 0) } pub fn is_match_all_with_offsets3( - _pattern: &str, - _subject: &str, - _matches: Option<&mut indexmap::IndexMap>>, + pattern: &str, + subject: &str, + matches: Option<&mut IndexMap>>, ) -> anyhow::Result { - todo!() + Ok(Self::match_all_with_offsets5(pattern, subject, matches, 0, 0)? > 0) } pub fn is_match_all_with_offsets4( - _pattern: &str, - _subject: &str, - _matches: Option<&mut indexmap::IndexMap>>, - _flags: i64, + pattern: &str, + subject: &str, + matches: Option<&mut IndexMap>>, + flags: i64, ) -> anyhow::Result { - todo!() + Ok(Self::match_all_with_offsets5(pattern, subject, matches, flags, 0)? > 0) } pub fn is_match_all_with_offsets5( - _pattern: &str, - _subject: &str, - _matches: Option<&mut indexmap::IndexMap>>, - _flags: i64, - _offset: usize, + pattern: &str, + subject: &str, + matches: Option<&mut IndexMap>>, + flags: i64, + offset: usize, ) -> anyhow::Result { - todo!() + Ok(Self::match_all_with_offsets5(pattern, subject, matches, flags, offset)? > 0) + } + + fn check_offset_capture(flags: i64, use_function_name: &str) { + assert!( + flags & PREG_OFFSET_CAPTURE == 0, + "PREG_OFFSET_CAPTURE is not supported as it changes the type of $matches, use {}() instead", + use_function_name + ); + } + + fn check_set_order(flags: i64) { + assert!( + flags & PREG_SET_ORDER == 0, + "PREG_SET_ORDER is not supported as it changes the type of $matches" + ); + } + + fn enforce_non_null_matches( + pattern: &str, + matches: IndexMap>, + variant_method: &str, + ) -> anyhow::Result> { + let mut result = IndexMap::new(); + for (group, m) in matches { + match m { + None => { + return Err(UnexpectedNullMatchException::new(format!( + "Pattern \"{}\" had an unexpected unmatched group \"{}\", make sure the pattern always matches or use {}() instead.", + pattern, + capture_key_to_string(&group), + variant_method + )) + .into()); + } + Some(value) => { + result.insert(group, value); + } + } + } + Ok(result) + } + + fn enforce_non_null_match_all( + pattern: &str, + matches: IndexMap>>, + variant_method: &str, + ) -> anyhow::Result>> { + let mut result = IndexMap::new(); + for (group, group_matches) in matches { + let mut converted = Vec::with_capacity(group_matches.len()); + for m in group_matches { + match m { + None => { + return Err(UnexpectedNullMatchException::new(format!( + "Pattern \"{}\" had an unexpected unmatched group \"{}\", make sure the pattern always matches or use {}() instead.", + pattern, + capture_key_to_string(&group), + variant_method + )) + .into()); + } + Some(value) => converted.push(value), + } + } + result.insert(group, converted); + } + Ok(result) + } +} + +fn capture_key_to_string(key: &CaptureKey) -> String { + match key { + CaptureKey::ByIndex(index) => index.to_string(), + CaptureKey::ByName(name) => name.clone(), } } +// Drops `null` (unmatched) groups, mirroring how the public `string`-valued +// `matches` map represents PHP's `string|null` entries by their absence. +fn drop_null_matches( + matches: IndexMap>, +) -> IndexMap { + matches + .into_iter() + .filter_map(|(key, value)| value.map(|value| (key, value))) + .collect() +} + +fn drop_null_matches_ref( + matches: &IndexMap>, +) -> IndexMap { + matches + .iter() + .filter_map(|(key, value)| value.clone().map(|value| (key.clone(), value))) + .collect() +} + +fn drop_null_offset_matches( + matches: IndexMap, i64)>, +) -> IndexMap { + matches + .into_iter() + .filter_map(|(key, (value, offset))| { + value.map(|value| (key, (value, offset.max(0) as usize))) + }) + .collect() +} + +// In the `Vec`-valued maps a per-iteration `null` cannot be stored, so +// unmatched groups collapse to "" (the classic non-PREG_UNMATCHED_AS_NULL form). +fn null_to_empty_match_all( + matches: IndexMap>>, +) -> IndexMap> { + matches + .into_iter() + .map(|(key, values)| { + ( + key, + values + .into_iter() + .map(|value| value.unwrap_or_default()) + .collect(), + ) + }) + .collect() +} + +fn null_to_empty_offset_match_all( + matches: IndexMap, i64)>>, +) -> IndexMap> { + matches + .into_iter() + .map(|(key, values)| { + ( + key, + values + .into_iter() + .map(|(value, offset)| (value.unwrap_or_default(), offset.max(0) as usize)) + .collect(), + ) + }) + .collect() +} + #[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Hash)] pub enum CaptureKey { ByIndex(usize), 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>>, + _flags: i64, + _offset: usize, +) -> Option { + todo!() +} + +pub fn preg_match_offset_capture( + _pattern: &str, + _subject: &str, + _matches: Option<&mut IndexMap, i64)>>, + _flags: i64, + _offset: usize, +) -> Option { + todo!() +} + +pub fn preg_match_all( + _pattern: &str, + _subject: &str, + _matches: Option<&mut IndexMap>>>, + _flags: i64, + _offset: usize, +) -> Option { + todo!() +} + +pub fn preg_match_all_offset_capture( + _pattern: &str, + _subject: &str, + _matches: Option<&mut IndexMap, i64)>>>, + _flags: i64, + _offset: usize, +) -> Option { + todo!() +} + +pub fn preg_replace( + _pattern: &str, + _replacement: &str, + _subject: &str, + _limit: i64, + _count: Option<&mut usize>, +) -> Option { + todo!() +} + +pub fn preg_replace_callback>) -> String>( + _pattern: &str, + _callback: F, + _subject: &str, + _limit: i64, + _count: Option<&mut usize>, + _flags: i64, +) -> Option { + todo!() +} + +pub fn preg_split(_pattern: &str, _subject: &str, _limit: i64, _flags: i64) -> Option> { + todo!() +} + +pub fn preg_split_offset_capture( + _pattern: &str, + _subject: &str, + _limit: i64, + _flags: i64, +) -> Option> { + todo!() +} + +pub fn preg_grep(_pattern: &str, _array: &[&str], _flags: i64) -> Option> { + todo!() +} diff --git a/crates/shirabe-external-packages/src/composer/pcre/unexpected_null_match_exception.rs b/crates/shirabe-external-packages/src/composer/pcre/unexpected_null_match_exception.rs new file mode 100644 index 0000000..513870c --- /dev/null +++ b/crates/shirabe-external-packages/src/composer/pcre/unexpected_null_match_exception.rs @@ -0,0 +1,29 @@ +//! ref: composer/vendor/composer/pcre/src/UnexpectedNullMatchException.php + +use super::pcre_exception::PcreException; + +#[derive(Debug)] +pub struct UnexpectedNullMatchException(pub PcreException); + +impl UnexpectedNullMatchException { + pub fn new(message: String) -> UnexpectedNullMatchException { + UnexpectedNullMatchException(PcreException(shirabe_php_shim::RuntimeException { + message, + code: 0, + })) + } + + pub fn from_function(_function: &str, _pattern: &str) -> UnexpectedNullMatchException { + panic!( + "fromFunction should not be called on UnexpectedNullMatchException, use PcreException" + ); + } +} + +impl std::fmt::Display for UnexpectedNullMatchException { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl std::error::Error for UnexpectedNullMatchException {} -- cgit v1.3.1