diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-06-14 16:39:38 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-06-14 22:19:30 +0900 |
| commit | 7f3171b14a89762b2f0c71969d7243f2297af7c9 (patch) | |
| tree | 37253f75e4f3b1c0e8a3c4161f7186bbe2c1a2d7 /crates | |
| parent | ae113f91fefff415898781272b892d670fcb46f4 (diff) | |
| download | php-shirabe-7f3171b14a89762b2f0c71969d7243f2297af7c9.tar.gz php-shirabe-7f3171b14a89762b2f0c71969d7243f2297af7c9.tar.zst php-shirabe-7f3171b14a89762b2f0c71969d7243f2297af7c9.zip | |
feat(php-shim): implement more shim functions
Diffstat (limited to 'crates')
5 files changed, 96 insertions, 25 deletions
diff --git a/crates/shirabe-external-packages/src/composer/xdebug_handler/xdebug_handler.rs b/crates/shirabe-external-packages/src/composer/xdebug_handler/xdebug_handler.rs index 9442d08..388d557 100644 --- a/crates/shirabe-external-packages/src/composer/xdebug_handler/xdebug_handler.rs +++ b/crates/shirabe-external-packages/src/composer/xdebug_handler/xdebug_handler.rs @@ -5,7 +5,8 @@ pub struct XdebugHandler; impl XdebugHandler { pub fn is_xdebug_active() -> bool { - todo!() + // TODO(php-runtime) + false } pub fn get_skipped_version() -> Option<String> { diff --git a/crates/shirabe-external-packages/src/symfony/console/input/input_option.rs b/crates/shirabe-external-packages/src/symfony/console/input/input_option.rs index 4b3feb8..afb36a5 100644 --- a/crates/shirabe-external-packages/src/symfony/console/input/input_option.rs +++ b/crates/shirabe-external-packages/src/symfony/console/input/input_option.rs @@ -110,7 +110,7 @@ impl InputOption { fn normalize_shortcut(s: String) -> anyhow::Result<Option<String>> { let stripped = shirabe_php_shim::ltrim(&s, Some("-")); - let parts = shirabe_php_shim::preg_split(r"\|(-?)", &stripped); + let parts = shirabe_php_shim::preg_split(r"{(\|)-?}", &stripped); let filtered: Vec<String> = shirabe_php_shim::array_filter(&parts, |s: &String| !s.is_empty()); let result = shirabe_php_shim::implode("|", &filtered); diff --git a/crates/shirabe-php-shim/src/lib.rs b/crates/shirabe-php-shim/src/lib.rs index c236727..cb99546 100644 --- a/crates/shirabe-php-shim/src/lib.rs +++ b/crates/shirabe-php-shim/src/lib.rs @@ -1592,9 +1592,9 @@ pub fn curl_handle_id(_handle: &CurlHandle) -> i64 { todo!() } -pub fn restore_error_handler() { - todo!() -} +// TODO(php-runtime): the previous handler should be restored in the PHP runtime. +// Paired with set_error_handler, which is a no-op in this shim. +pub fn restore_error_handler() {} pub fn stream_get_contents_with_max(stream: PhpMixed, max_length: Option<i64>) -> Option<String> { let _ = (stream, max_length); @@ -1610,7 +1610,9 @@ pub fn is_dir(_path: &str) -> bool { } pub fn file_get_contents(_path: &str) -> Option<String> { - todo!() + std::fs::read(_path) + .ok() + .map(|bytes| String::from_utf8_lossy(&bytes).into_owned()) } pub fn file_get_contents5( @@ -1795,6 +1797,12 @@ pub fn php_uname(mode: &str) -> String { other => other, } .to_string(), + // TODO(phase-c): use libc? + // release, as reported by uname(2). On Linux this matches the contents + // of /proc/sys/kernel/osrelease. + "r" => std::fs::read_to_string("/proc/sys/kernel/osrelease") + .map(|s| s.trim_end().to_string()) + .unwrap_or_default(), _ => todo!(), } } @@ -1921,8 +1929,44 @@ pub fn strcmp(_s1: &str, _s2: &str) -> i64 { _s1.cmp(_s2) as i64 } -pub fn rtrim(_s: &str, _chars: Option<&str>) -> String { - todo!() +/// PHP's default trim character mask: " \t\n\r\0\x0B". +const PHP_TRIM_DEFAULT_CHARS: &[u8] = b" \t\n\r\0\x0B"; + +/// Build the set of bytes to strip from a PHP trim `$characters` argument, +/// expanding `a..b` range syntax as PHP does. +fn php_trim_mask(chars: &[u8]) -> [bool; 256] { + let mut mask = [false; 256]; + let mut i = 0; + while i < chars.len() { + if i + 3 < chars.len() && chars[i + 1] == b'.' && chars[i + 2] == b'.' { + let start = chars[i]; + let end = chars[i + 3]; + if start <= end { + for b in start..=end { + mask[b as usize] = true; + } + i += 4; + continue; + } + } + mask[chars[i] as usize] = true; + i += 1; + } + mask +} + +pub fn rtrim(s: &str, chars: Option<&str>) -> String { + let mask = php_trim_mask( + chars + .map(|c| c.as_bytes()) + .unwrap_or(PHP_TRIM_DEFAULT_CHARS), + ); + let bytes = s.as_bytes(); + let mut end = bytes.len(); + while end > 0 && mask[bytes[end - 1] as usize] { + end -= 1; + } + String::from_utf8_lossy(&bytes[..end]).into_owned() } pub fn rmdir(_dir: &str) -> bool { @@ -2227,7 +2271,11 @@ pub fn memory_get_usage() -> i64 { } pub fn mb_check_encoding(_value: &str, _encoding: &str) -> bool { - todo!() + match _encoding.to_ascii_uppercase().replace('-', "").as_str() { + // A Rust &str is, by construction, valid UTF-8. + "UTF8" => true, + _ => todo!(), + } } pub fn iconv(_in_charset: &str, _out_charset: &str, _string: &str) -> Option<String> { @@ -2379,7 +2427,17 @@ pub fn error_get_last() -> Option<IndexMap<String, Box<PhpMixed>>> { } pub fn is_readable(_path: &str) -> bool { - todo!() + let path = std::path::Path::new(_path); + match std::fs::metadata(path) { + Ok(meta) => { + if meta.is_dir() { + std::fs::read_dir(path).is_ok() + } else { + std::fs::File::open(path).is_ok() + } + } + Err(_) => false, + } } pub fn stream_get_wrappers() -> Vec<String> { diff --git a/crates/shirabe-php-shim/src/preg.rs b/crates/shirabe-php-shim/src/preg.rs index 1429f1c..46d9428 100644 --- a/crates/shirabe-php-shim/src/preg.rs +++ b/crates/shirabe-php-shim/src/preg.rs @@ -393,12 +393,21 @@ fn compile_php_pattern(pattern: &str) -> anyhow::Result<regex::Regex> { .chars() .next() .ok_or_else(|| anyhow::anyhow!("empty regex pattern"))?; + // PCRE allows bracket-style delimiters whose closing character differs from + // the opening one: `(...)`, `{...}`, `[...]`, `<...>`. + let closing = match delimiter { + '(' => ')', + '{' => '}', + '[' => ']', + '<' => '>', + c => c, + }; let end = pattern - .rfind(delimiter) - .filter(|&i| i > 0) + .rfind(closing) + .filter(|&i| i >= delimiter.len_utf8()) .ok_or_else(|| anyhow::anyhow!("unterminated regex pattern: {pattern}"))?; let inner = &pattern[delimiter.len_utf8()..end]; - let modifiers = &pattern[end + delimiter.len_utf8()..]; + let modifiers = &pattern[end + closing.len_utf8()..]; let flags: String = modifiers .chars() diff --git a/crates/shirabe/src/io/console_io.rs b/crates/shirabe/src/io/console_io.rs index 3afd300..ecb12b1 100644 --- a/crates/shirabe/src/io/console_io.rs +++ b/crates/shirabe/src/io/console_io.rs @@ -275,10 +275,6 @@ impl ConsoleIO { /// All other control chars (except NULL bytes) as well as ANSI escape sequences are removed. /// /// Invalid unicode sequences are turned into question marks. - /// - /// @param string|iterable<string> $messages - /// @return string|array<string> - /// @phpstan-return ($messages is string ? string : array<string>) pub fn sanitize(messages: PhpMixed, allow_newlines: bool) -> PhpMixed { // Match ANSI escape sequences: // - CSI (Control Sequence Introducer): ESC [ params intermediate final @@ -286,17 +282,24 @@ impl ConsoleIO { // - Other ESC sequences: ESC followed by any character let escape_pattern = r"\x1B\[[\x30-\x3F]*[\x20-\x2F]*[\x40-\x7E]|\x1B\].*?(?:\x1B\\|\x07)|\x1B."; - let pattern = if allow_newlines { - format!( - "{{{}|[\\x01-\\x09\\x0B\\x0C\\x0E-\\x1A]|\\r(?!\\n)}}u", - escape_pattern + // Regex pattern compatibility: + // PCRE's `\r(?!\n)` (non-CRLF CR) uses a negative lookahead, which the regex crate does + // not support. Instead we capture `\r\n` and re-emit it via the `$1` backreference. A CRLF + // pair is preserved while a lone CR is removed. + let (pattern, replacement) = if allow_newlines { + ( + format!( + r#"@{}|[\x01-\x09\x0B\x0C\x0E-\x1A]|(\r\n)|\r@u"#, + escape_pattern + ), + "$1", ) } else { - format!("{{{}|[\\x01-\\x1A]}}u", escape_pattern) + (format!("{{{}|[\\x01-\\x1A]}}u", escape_pattern), "") }; if is_string(&messages) { let message = Self::ensure_valid_utf8(messages.as_string().unwrap_or("")); - return PhpMixed::String(Preg::replace(&pattern, "", &message)); + return PhpMixed::String(Preg::replace(&pattern, replacement, &message)); } // PHP: $sanitized = []; foreach ($messages as $key => $message) { ... } @@ -307,7 +310,7 @@ impl ConsoleIO { let s = Self::ensure_valid_utf8(message.as_string().unwrap_or("")); sanitized.insert( key.to_string(), - PhpMixed::String(Preg::replace(&pattern, "", &s)), + PhpMixed::String(Preg::replace(&pattern, replacement, &s)), ); } } @@ -316,7 +319,7 @@ impl ConsoleIO { let s = Self::ensure_valid_utf8(message.as_string().unwrap_or("")); sanitized.insert( key.clone(), - PhpMixed::String(Preg::replace(&pattern, "", &s)), + PhpMixed::String(Preg::replace(&pattern, replacement, &s)), ); } } |
