diff options
23 files changed, 790 insertions, 767 deletions
diff --git a/crates/shirabe-php-rpc/php/worker.php b/crates/shirabe-php-rpc/php/worker.php index 15b38770..578c30d0 100644 --- a/crates/shirabe-php-rpc/php/worker.php +++ b/crates/shirabe-php-rpc/php/worker.php @@ -455,6 +455,77 @@ ShirabeRpcRuntime::$stubAutoloader = static function (string $class): void { }; spl_autoload_register(ShirabeRpcRuntime::$stubAutoloader, true, true); +/** + * Port of Composer\Platform\Runtime, whose runtime queries feed the `platform` payload. Its + * parseHtmlExtensionInfo() has no counterpart here: the worker is always the CLI SAPI, so + * getExtensionInfo() never takes the branch that reformats phpinfo()'s HTML output. + */ +final class ShirabePlatformRuntime +{ + /** The constants the `platform` payload reports, named ltrim($class.'::'.$constant, ':'). */ + public const CONSTANTS = [ + 'PHP_VERSION', + 'PHP_DEBUG', + 'PHP_ZTS', + 'PHP_INT_SIZE', + 'AF_INET6', + 'GD_VERSION', + 'GMP_VERSION', + 'ICONV_VERSION', + 'INTL_ICU_VERSION', + 'LIBXML_DOTTED_VERSION', + 'MB_ONIGURUMA_VERSION', + 'OPENSSL_VERSION_TEXT', + 'PCRE_VERSION', + 'PGSQL_LIBPQ_VERSION', + 'RD_KAFKA_VERSION', + 'SODIUM_LIBRARY_VERSION', + 'LIBXSLT_DOTTED_VERSION', + 'ZipArchive::LIBZIP_VERSION', + 'ZLIB_VERSION', + ]; + + /** The extensions whose info() output the `platform` payload reports when they are loaded. */ + public const EXTENSION_INFO = [ + 'amqp', + 'bz2', + 'curl', + 'date', + 'fileinfo', + 'gd', + 'intl', + 'ldap', + 'mbstring', + 'memcached', + 'mongodb', + 'mysqlnd', + 'pcre', + 'pdo_mysql', + 'pdo_pgsql', + 'pdo_sqlite', + 'pgsql', + 'pq', + 'sqlite3', + 'ssh2', + 'xsl', + 'yaml', + 'zlib', + ]; + + /** The classes the `platform` payload reports the existence of. */ + public const CLASSES = ['ResourceBundle', 'IntlChar']; + + public static function getExtensionInfo(string $extension): string + { + $reflector = new ReflectionExtension($extension); + + ob_start(); + $reflector->info(); + + return (string) ob_get_clean(); + } +} + // Port of Composer\XdebugHandler\XdebugHandler::setXdebugDetails(), which the diagnose payload // reports as `xdebug_active`. $xdebug_active = static function (): bool { @@ -491,9 +562,6 @@ $xdebug_active = static function (): bool { ShirabeRpcRuntime::$dispatch = [ 'constant' => static fn($args) => defined($args[0]) ? constant($args[0]) : null, - 'inet_pton' => static fn($args) => @inet_pton($args[0]), - 'curl_version' => static fn($args) => function_exists('curl_version') ? (curl_version()['version'] ?? null) : null, - 'get_loaded_extensions' => static fn($args) => get_loaded_extensions(), 'get_all_ini_files' => static function ($args) { $paths = [(string) php_ini_loaded_file()]; $scanned = php_ini_scanned_files(); @@ -502,14 +570,60 @@ ShirabeRpcRuntime::$dispatch = [ } return $paths; }, - 'extension_info' => static function ($args) { - if (!extension_loaded($args[0])) { - return ''; + 'platform' => static function ($args) { + $extensions = get_loaded_extensions(); + + $extension_versions = []; + foreach ($extensions as $extension) { + $version = phpversion($extension); + $extension_versions[$extension] = $version !== false ? $version : '0'; } - $re = new ReflectionExtension($args[0]); - ob_start(); - $re->info(); - return (string) ob_get_clean(); + + $extension_info = []; + foreach (ShirabePlatformRuntime::EXTENSION_INFO as $extension) { + if (in_array($extension, $extensions, true)) { + $extension_info[$extension] = ShirabePlatformRuntime::getExtensionInfo($extension); + } + } + + // Only the defined constants carry a value; `constant_names` tells the Rust side which + // names were looked up, so a name it reads but this list omits is an error rather than a + // silently undefined constant. + $constants = []; + foreach (ShirabePlatformRuntime::CONSTANTS as $constant) { + if (defined($constant)) { + $constants[$constant] = constant($constant); + } + } + + $classes = []; + foreach (ShirabePlatformRuntime::CLASSES as $class) { + $classes[$class] = class_exists($class, false); + } + + // The values below stand in for the PHP objects and calls PlatformRepository reaches + // through Composer\Platform\Runtime::invoke()/construct(), reduced to the entries it reads. + $resource_bundle = null; + if ($classes['ResourceBundle']) { + $bundle = ResourceBundle::create('root', 'ICUDATA', false); + if ($bundle !== null) { + $resource_bundle = ['Version' => $bundle->get('Version')]; + } + } + + return [ + 'extensions' => $extensions, + 'extension_versions' => $extension_versions, + 'extension_info' => $extension_info, + 'constant_names' => ShirabePlatformRuntime::CONSTANTS, + 'constants' => $constants, + 'classes' => $classes, + 'curl_version' => extension_loaded('curl') ? curl_version() : null, + 'inet_pton_ipv6' => @inet_pton('::'), + 'resource_bundle' => $resource_bundle, + 'intl_char_unicode_version' => $classes['IntlChar'] ? IntlChar::getUnicodeVersion() : null, + 'imagick' => extension_loaded('imagick') ? (new Imagick())->getVersion() : null, + ]; }, 'diagnose' => static function ($args) use ($xdebug_active) { $extensions = []; diff --git a/crates/shirabe-php-rpc/src/lib.rs b/crates/shirabe-php-rpc/src/lib.rs index 69790695..e2f5fd1e 100644 --- a/crates/shirabe-php-rpc/src/lib.rs +++ b/crates/shirabe-php-rpc/src/lib.rs @@ -30,49 +30,11 @@ pub fn get_php_binary() -> String { } } -/// PHP `defined($name)`. -pub fn has_constant(name: &str) -> bool { - match call("defined", name) { - PhpMixed::Bool(b) => b, - other => panic!("PHP RPC: `defined` did not return a bool: {other:?}"), - } -} - /// PHP `constant($name)`. -pub fn get_constant(name: &str) -> PhpMixed { +fn get_constant(name: &str) -> PhpMixed { call("constant", name) } -/// PHP `class_exists($name)`, with autoloading, as the runtime sees it. -pub fn class_exists(name: &str) -> bool { - match call("class_exists", name) { - PhpMixed::Bool(exists) => exists, - other => panic!("PHP RPC: `class_exists` returned an unexpected value: {other:?}"), - } -} - -/// PHP `inet_pton($address)`. -pub fn inet_pton(address: &str) -> PhpMixed { - call("inet_pton", address) -} - -/// PHP `curl_version()['version']`. -pub fn curl_version() -> Option<String> { - match call("curl_version", "") { - PhpMixed::String(s) => Some(s), - PhpMixed::Null => None, - other => panic!("PHP RPC: `curl_version` returned an unexpected value: {other:?}"), - } -} - -/// PHP `(new \ReflectionExtension($name))->info()` output. -pub fn get_extension_info(name: &str) -> String { - match call("extension_info", name) { - PhpMixed::String(s) => s, - other => panic!("PHP RPC: `extension_info` did not return a string: {other:?}"), - } -} - /// `curl_version()`, together with the `CURL_*` constants the `diagnose` command consults. Every /// `Option` field is `None` when the corresponding array key or constant is absent. #[derive(Debug)] @@ -89,7 +51,7 @@ pub struct Curl { } /// Everything the `diagnose` command needs to know about the PHP runtime, fetched in a single -/// round trip because the command would otherwise probe the same runtime dozens of times. +/// round trip because the command would otherwise query the same runtime dozens of times. #[derive(Debug)] pub struct Diagnostics { pub php_version: String, @@ -118,28 +80,28 @@ pub struct Diagnostics { } impl Diagnostics { - /// PHP `extension_loaded($name)`. Only the extensions the worker probes can be asked about; + /// PHP `extension_loaded($name)`. Only the extensions the worker reports can be asked about; /// any other name is a bug in the caller, not a missing extension. pub fn extension_loaded(&self, name: &str) -> bool { *self.extensions.get(name).unwrap_or_else(|| { - panic!("PHP RPC: extension `{name}` is not probed by the diagnose payload") + panic!("PHP RPC: extension `{name}` is not reported by the diagnose payload") }) } - /// PHP `function_exists($name)`. See [`Diagnostics::extension_loaded`] for the fixed probe set. + /// PHP `function_exists($name)`. See [`Diagnostics::extension_loaded`] for the fixed set of names. pub fn function_exists(&self, name: &str) -> bool { *self.functions.get(name).unwrap_or_else(|| { - panic!("PHP RPC: function `{name}` is not probed by the diagnose payload") + panic!("PHP RPC: function `{name}` is not reported by the diagnose payload") }) } /// PHP `ini_get($option)`, with PHP's `false` (no such setting) mapped to `None`. See - /// [`Diagnostics::extension_loaded`] for the fixed probe set. + /// [`Diagnostics::extension_loaded`] for the fixed set of names. pub fn ini_get(&self, option: &str) -> Option<&str> { self.ini_settings .get(option) .unwrap_or_else(|| { - panic!("PHP RPC: ini setting `{option}` is not probed by the diagnose payload") + panic!("PHP RPC: ini setting `{option}` is not reported by the diagnose payload") }) .as_deref() } @@ -155,123 +117,416 @@ pub fn get_diagnostics() -> &'static Diagnostics { let payload = payload .as_array() .unwrap_or_else(|| panic!("PHP RPC: `diagnose` did not return an array: {payload:?}")); + let payload = &Payload { + what: "diagnose", + map: payload, + }; Diagnostics { - php_version: string_field(payload, "php_version"), - php_version_id: int_field(payload, "php_version_id"), - php_binary: nullable_string_field(payload, "php_binary"), - openssl_version_text: nullable_string_field(payload, "openssl_version_text"), - openssl_version_number: int_field(payload, "openssl_version_number"), - has_hhvm_version: bool_field(payload, "has_hhvm_version"), - has_php_windows_version_build: bool_field(payload, "has_php_windows_version_build"), - xdebug_active: bool_field(payload, "xdebug_active"), - ioncube_loader_iversion: int_field(payload, "ioncube_loader_iversion"), - ioncube_loader_version: string_field(payload, "ioncube_loader_version"), - phpinfo_general: string_field(payload, "phpinfo_general"), + php_version: payload.string("php_version"), + php_version_id: payload.int("php_version_id"), + php_binary: payload.nullable_string("php_binary"), + openssl_version_text: payload.nullable_string("openssl_version_text"), + openssl_version_number: payload.int("openssl_version_number"), + has_hhvm_version: payload.bool("has_hhvm_version"), + has_php_windows_version_build: payload.bool("has_php_windows_version_build"), + xdebug_active: payload.bool("xdebug_active"), + ioncube_loader_iversion: payload.int("ioncube_loader_iversion"), + ioncube_loader_version: payload.string("ioncube_loader_version"), + phpinfo_general: payload.string("phpinfo_general"), curl: curl_field(payload, "curl"), - extensions: map_field(payload, "extensions") + extensions: payload.bool_map("extensions"), + functions: payload.bool_map("functions"), + ini_settings: payload + .entries("ini") + .map(|(name, value)| (name.clone(), as_nullable_string(value, "ini", name))) + .collect(), + } + }) +} + +/// The constants the `platform` payload carries, in the worker's order. +const PLATFORM_CONSTANTS: &[&str] = &[ + "PHP_VERSION", + "PHP_DEBUG", + "PHP_ZTS", + "PHP_INT_SIZE", + "AF_INET6", + "GD_VERSION", + "GMP_VERSION", + "ICONV_VERSION", + "INTL_ICU_VERSION", + "LIBXML_DOTTED_VERSION", + "MB_ONIGURUMA_VERSION", + "OPENSSL_VERSION_TEXT", + "PCRE_VERSION", + "PGSQL_LIBPQ_VERSION", + "RD_KAFKA_VERSION", + "SODIUM_LIBRARY_VERSION", + "LIBXSLT_DOTTED_VERSION", + "ZipArchive::LIBZIP_VERSION", + "ZLIB_VERSION", +]; + +/// The classes the `platform` payload carries the existence of. +const PLATFORM_CLASSES: &[&str] = &["ResourceBundle", "IntlChar"]; + +/// Everything `PlatformRepository` needs to know about the PHP runtime, fetched in a single round +/// trip because it would otherwise query the same runtime once per extension and constant. +/// +/// The fields standing in for PHP objects (`resource_bundle`, `imagick`) carry the entries the +/// consumer reads off them rather than the object itself. +#[derive(Debug, Clone)] +pub struct PlatformInfo { + extensions: Vec<String>, + extension_versions: IndexMap<String, String>, + extension_info: IndexMap<String, String>, + /// Keyed as `ltrim($class.'::'.$constant, ':')`; `None` for a reported but undefined constant. + constants: IndexMap<String, Option<PhpMixed>>, + classes: IndexMap<String, bool>, + /// `curl_version()`, or null when the curl extension is not loaded. + pub curl_version: PhpMixed, + /// `@inet_pton('::')`. + pub inet_pton_ipv6: PhpMixed, + /// `['Version' => ResourceBundle::create('root', 'ICUDATA', false)->get('Version')]`, or null + /// when the class is absent or the bundle cannot be opened. + pub resource_bundle: PhpMixed, + /// `IntlChar::getUnicodeVersion()`, or null when the class is absent. + pub intl_char_unicode_version: PhpMixed, + /// `(new Imagick())->getVersion()`, or null when the extension is not loaded. + pub imagick: PhpMixed, +} + +impl Default for PlatformInfo { + /// A runtime with no extensions loaded, no classes defined and every reported constant + /// undefined. + fn default() -> Self { + PlatformInfo { + extensions: Vec::new(), + extension_versions: IndexMap::new(), + extension_info: IndexMap::new(), + constants: PLATFORM_CONSTANTS .iter() - .map(|(name, value)| (name.clone(), as_bool(value, name))) + .map(|name| ((*name).to_string(), None)) .collect(), - functions: map_field(payload, "functions") + classes: PLATFORM_CLASSES .iter() - .map(|(name, value)| (name.clone(), as_bool(value, name))) + .map(|name| ((*name).to_string(), false)) .collect(), - ini_settings: map_field(payload, "ini") + curl_version: PhpMixed::Null, + inet_pton_ipv6: PhpMixed::Null, + resource_bundle: PhpMixed::Null, + intl_char_unicode_version: PhpMixed::Null, + imagick: PhpMixed::Null, + } + } +} + +impl PlatformInfo { + /// PHP `get_loaded_extensions()`. + pub fn get_extensions(&self) -> &[String] { + &self.extensions + } + + /// PHP `phpversion($extension)`, with PHP's `false` mapped to `'0'`. + pub fn get_extension_version(&self, extension: &str) -> &str { + self.extension_versions + .get(extension) + .unwrap_or_else(|| { + panic!("PHP RPC: extension `{extension}` is not loaded in the platform payload") + }) + .as_str() + } + + /// PHP `(new \ReflectionExtension($extension))->info()` output. Only the extensions the worker + /// reports can be asked about; any other name is a bug in the caller. + pub fn get_extension_info(&self, extension: &str) -> &str { + self.extension_info + .get(extension) + .unwrap_or_else(|| { + panic!("PHP RPC: extension `{extension}` info is not in the platform payload") + }) + .as_str() + } + + /// PHP `defined(ltrim($class.'::'.$constant, ':'))`. Only the constants the worker reports can + /// be asked about; any other name is a bug in the caller, not an undefined constant. + pub fn has_constant(&self, constant_name: &str, class: Option<&str>) -> bool { + self.constant(constant_name, class).is_some() + } + + /// PHP `constant(ltrim($class.'::'.$constant, ':'))`, answering null for an undefined + /// constant. See [`PlatformInfo::has_constant`] for the fixed set of names. + pub fn get_constant(&self, constant_name: &str, class: Option<&str>) -> PhpMixed { + self.constant(constant_name, class) + .cloned() + .unwrap_or(PhpMixed::Null) + } + + /// PHP `class_exists($class, false)`. See [`PlatformInfo::has_constant`] for the fixed set of + /// names. + pub fn has_class(&self, class: &str) -> bool { + *self.classes.get(class).unwrap_or_else(|| { + panic!("PHP RPC: class `{class}` is not reported by the platform payload") + }) + } + + /// For testing only: reports `extensions` as loaded, each at `version`. + pub fn __set_extensions(&mut self, extensions: Vec<String>, version: &str) { + self.extension_versions = extensions + .iter() + .map(|name| (name.clone(), version.to_string())) + .collect(); + self.extensions = extensions; + } + + /// For testing only: reports `info` as the `ReflectionExtension::info()` output of `extension`. + pub fn __set_extension_info(&mut self, extension: &str, info: &str) { + self.extension_info + .insert(extension.to_string(), info.to_string()); + } + + /// For testing only: reports the constant as defined with `value`. Panics on a constant the + /// worker does not report, so a test cannot describe a runtime the worker cannot report. + pub fn __set_constant(&mut self, constant_name: &str, class: Option<&str>, value: PhpMixed) { + let key = Self::constant_key(constant_name, class); + let entry = self.constants.get_mut(&key).unwrap_or_else(|| { + panic!("PHP RPC: constant `{key}` is not reported by the platform payload") + }); + *entry = Some(value); + } + + /// For testing only: reports the class as defined. See [`PlatformInfo::__set_constant`]. + pub fn __set_class(&mut self, class: &str) { + let entry = self.classes.get_mut(class).unwrap_or_else(|| { + panic!("PHP RPC: class `{class}` is not reported by the platform payload") + }); + *entry = true; + } + + fn constant(&self, constant_name: &str, class: Option<&str>) -> Option<&PhpMixed> { + let key = Self::constant_key(constant_name, class); + self.constants + .get(&key) + .unwrap_or_else(|| { + panic!("PHP RPC: constant `{key}` is not reported by the platform payload") + }) + .as_ref() + } + + fn constant_key(constant_name: &str, class: Option<&str>) -> String { + match class { + Some(class) => format!("{class}::{constant_name}"), + None => constant_name.to_string(), + } + } +} + +static PLATFORM_INFO: OnceLock<PlatformInfo> = OnceLock::new(); + +/// PHP runtime information for `PlatformRepository`. The worker is queried once per process; +/// subsequent calls reuse the cached payload. +pub fn get_platform_info() -> &'static PlatformInfo { + PLATFORM_INFO.get_or_init(|| { + let payload = call("platform", ""); + let payload = payload + .as_array() + .unwrap_or_else(|| panic!("PHP RPC: `platform` did not return an array: {payload:?}")); + let payload = &Payload { + what: "platform", + map: payload, + }; + + let constant_names = payload.string_list("constant_names"); + assert_eq!( + constant_names .iter() - .map(|(name, value)| (name.clone(), as_nullable_string(value, name))) + .map(String::as_str) + .collect::<Vec<_>>(), + PLATFORM_CONSTANTS, + "PHP RPC: the worker reports different constants than the platform payload declares" + ); + let constants = payload.map("constants"); + let classes = payload.bool_map("classes"); + assert_eq!( + classes.keys().map(String::as_str).collect::<Vec<_>>(), + PLATFORM_CLASSES, + "PHP RPC: the worker reports different classes than the platform payload declares" + ); + + PlatformInfo { + extensions: payload.string_list("extensions"), + extension_versions: payload.string_map("extension_versions"), + extension_info: payload.string_map("extension_info"), + constants: constant_names + .into_iter() + .map(|name| { + let value = constants.and_then(|map| map.get(&name)).cloned(); + (name, value) + }) .collect(), + classes, + curl_version: payload.mixed("curl_version"), + inet_pton_ipv6: payload.mixed("inet_pton_ipv6"), + resource_bundle: payload.mixed("resource_bundle"), + intl_char_unicode_version: payload.mixed("intl_char_unicode_version"), + imagick: payload.mixed("imagick"), } }) } -fn field<'a>(payload: &'a IndexMap<String, PhpMixed>, key: &str) -> &'a PhpMixed { - payload - .get(key) - .unwrap_or_else(|| panic!("PHP RPC: `diagnose` payload has no `{key}` entry")) +/// One worker answer, named after the dispatch entry that produced it so a decoding failure says +/// which payload was malformed. +struct Payload<'a> { + what: &'static str, + map: &'a IndexMap<String, PhpMixed>, } -fn string_field(payload: &IndexMap<String, PhpMixed>, key: &str) -> String { - match field(payload, key) { - PhpMixed::String(s) => s.clone(), - other => panic!("PHP RPC: `diagnose` payload entry `{key}` is not a string: {other:?}"), +impl Payload<'_> { + fn field(&self, key: &str) -> &PhpMixed { + self.map + .get(key) + .unwrap_or_else(|| panic!("PHP RPC: `{}` payload has no `{key}` entry", self.what)) } -} -fn nullable_string_field(payload: &IndexMap<String, PhpMixed>, key: &str) -> Option<String> { - as_nullable_string(field(payload, key), key) -} + fn mixed(&self, key: &str) -> PhpMixed { + self.field(key).clone() + } -fn int_field(payload: &IndexMap<String, PhpMixed>, key: &str) -> i64 { - match field(payload, key) { - PhpMixed::Int(n) => *n, - other => panic!("PHP RPC: `diagnose` payload entry `{key}` is not an int: {other:?}"), + fn string(&self, key: &str) -> String { + match self.field(key) { + PhpMixed::String(s) => s.clone(), + other => panic!( + "PHP RPC: `{}` payload entry `{key}` is not a string: {other:?}", + self.what + ), + } } -} -fn bool_field(payload: &IndexMap<String, PhpMixed>, key: &str) -> bool { - as_bool(field(payload, key), key) -} + fn nullable_string(&self, key: &str) -> Option<String> { + as_nullable_string(self.field(key), self.what, key) + } -fn map_field<'a>( - payload: &'a IndexMap<String, PhpMixed>, - key: &str, -) -> &'a IndexMap<String, PhpMixed> { - match field(payload, key) { - PhpMixed::Array(map) => map, - other => panic!("PHP RPC: `diagnose` payload entry `{key}` is not an array: {other:?}"), + fn int(&self, key: &str) -> i64 { + match self.field(key) { + PhpMixed::Int(n) => *n, + other => panic!( + "PHP RPC: `{}` payload entry `{key}` is not an int: {other:?}", + self.what + ), + } + } + + fn bool(&self, key: &str) -> bool { + as_bool(self.field(key), self.what, key) + } + + /// `None` for PHP's empty array, which carries no key type and so decodes as an empty list. + fn map(&self, key: &str) -> Option<&IndexMap<String, PhpMixed>> { + match self.field(key) { + PhpMixed::Array(map) => Some(map), + PhpMixed::List(items) if items.is_empty() => None, + other => panic!( + "PHP RPC: `{}` payload entry `{key}` is not an array: {other:?}", + self.what + ), + } + } + + fn entries(&self, key: &str) -> impl Iterator<Item = (&String, &PhpMixed)> { + self.map(key).into_iter().flatten() + } + + fn bool_map(&self, key: &str) -> IndexMap<String, bool> { + self.entries(key) + .map(|(name, value)| (name.clone(), as_bool(value, self.what, name))) + .collect() + } + + fn string_map(&self, key: &str) -> IndexMap<String, String> { + self.entries(key) + .map(|(name, value)| match value { + PhpMixed::String(s) => (name.clone(), s.clone()), + other => panic!( + "PHP RPC: `{}` payload entry `{key}[{name}]` is not a string: {other:?}", + self.what + ), + }) + .collect() + } + + fn string_list(&self, key: &str) -> Vec<String> { + match self.field(key) { + PhpMixed::List(items) => items + .iter() + .map(|item| match item { + PhpMixed::String(s) => s.clone(), + other => panic!( + "PHP RPC: `{}` payload entry `{key}` has a non-string element: {other:?}", + self.what + ), + }) + .collect(), + other => panic!( + "PHP RPC: `{}` payload entry `{key}` is not a list: {other:?}", + self.what + ), + } } } -fn as_bool(value: &PhpMixed, key: &str) -> bool { +fn as_bool(value: &PhpMixed, what: &str, key: &str) -> bool { match value { PhpMixed::Bool(b) => *b, - other => panic!("PHP RPC: `diagnose` payload entry `{key}` is not a bool: {other:?}"), + other => panic!("PHP RPC: `{what}` payload entry `{key}` is not a bool: {other:?}"), + } +} + +fn as_nullable_string(value: &PhpMixed, what: &str, key: &str) -> Option<String> { + match value { + PhpMixed::String(s) => Some(s.clone()), + PhpMixed::Null => None, + other => { + panic!("PHP RPC: `{what}` payload entry `{key}` is not a string or null: {other:?}") + } } } -fn nullable_int_field(payload: &IndexMap<String, PhpMixed>, key: &str) -> Option<i64> { - match field(payload, key) { +fn nullable_int_field(payload: &Payload, key: &str) -> Option<i64> { + let what = payload.what; + match payload.field(key) { PhpMixed::Int(n) => Some(*n), PhpMixed::Null => None, other => { - panic!("PHP RPC: `diagnose` payload entry `{key}` is not an int or null: {other:?}") + panic!("PHP RPC: `{what}` payload entry `{key}` is not an int or null: {other:?}") } } } -fn curl_field(payload: &IndexMap<String, PhpMixed>, key: &str) -> Option<Curl> { - let curl = match field(payload, key) { +fn curl_field(payload: &Payload, key: &str) -> Option<Curl> { + let what = payload.what; + let curl = match payload.field(key) { PhpMixed::Null => return None, PhpMixed::Array(map) => map, other => { - panic!("PHP RPC: `diagnose` payload entry `{key}` is not an array or null: {other:?}") + panic!("PHP RPC: `{what}` payload entry `{key}` is not an array or null: {other:?}") } }; + let curl = &Payload { what, map: curl }; Some(Curl { - version: string_field(curl, "version"), - libz_version: nullable_string_field(curl, "libz_version"), - brotli_version: nullable_string_field(curl, "brotli_version"), - ssl_version: nullable_string_field(curl, "ssl_version"), + version: curl.string("version"), + libz_version: curl.nullable_string("libz_version"), + brotli_version: curl.nullable_string("brotli_version"), + ssl_version: curl.nullable_string("ssl_version"), features: nullable_int_field(curl, "features"), version_zstd: nullable_int_field(curl, "version_zstd"), version_http2: nullable_int_field(curl, "version_http2"), - has_http_version_2_0: bool_field(curl, "has_http_version_2_0"), + has_http_version_2_0: curl.bool("has_http_version_2_0"), version_http3: nullable_int_field(curl, "version_http3"), }) } -fn as_nullable_string(value: &PhpMixed, key: &str) -> Option<String> { - match value { - PhpMixed::String(s) => Some(s.clone()), - PhpMixed::Null => None, - other => { - panic!("PHP RPC: `diagnose` payload entry `{key}` is not a string or null: {other:?}") - } - } -} - /// PHP `phpversion($extension)`. pub fn phpversion(extension: &str) -> Option<String> { match call("phpversion", extension) { @@ -281,11 +536,6 @@ pub fn phpversion(extension: &str) -> Option<String> { } } -/// PHP `get_loaded_extensions()`. -pub fn get_loaded_extensions() -> Vec<String> { - string_list(call("get_loaded_extensions", ""), "get_loaded_extensions") -} - /// `Composer\XdebugHandler\XdebugHandler::getAllIniFiles()` (minus the `self::$name` branch, /// which is unreachable since this port never constructs an XdebugHandler): `[(string) /// php_ini_loaded_file()]` merged with the trimmed, comma-split `php_ini_scanned_files()` list @@ -851,12 +1101,6 @@ mod tests { return; } - let extensions = get_loaded_extensions(); - assert!( - extensions.iter().any(|extension| extension == "Core"), - "expected the Core extension among {extensions:?}", - ); - // XdebugHandler::getAllIniFiles() always yields at least one entry, which is the empty // string when no php.ini is loaded. assert!(!get_all_ini_files().is_empty()); @@ -910,20 +1154,38 @@ mod tests { } #[test] - fn queries_constants_when_php_available() { + fn queries_platform_info_when_php_available() { if PhpExecutableFinder::new().find(false).is_none() { // No PHP in this environment; the worker cannot start. return; } - assert!(has_constant("PHP_VERSION")); - assert!(!has_constant("SHIRABE_DOES_NOT_EXIST_XYZ")); + let platform_info = get_platform_info(); - assert_eq!(get_constant("PHP_INT_SIZE"), PhpMixed::Int(8)); - assert_eq!(get_constant("SHIRABE_DOES_NOT_EXIST_XYZ"), PhpMixed::Null); - match get_constant("PHP_VERSION") { + let extensions = platform_info.get_extensions(); + assert!( + extensions.iter().any(|extension| extension == "Core"), + "expected the Core extension among {extensions:?}", + ); + assert_eq!( + platform_info.get_extension_version("Core"), + get_php_version() + ); + + assert!(platform_info.has_constant("PHP_VERSION", None)); + assert_eq!( + platform_info.get_constant("PHP_INT_SIZE", None), + PhpMixed::Int(8) + ); + match platform_info.get_constant("PHP_VERSION", None) { PhpMixed::String(s) => assert!(!s.is_empty(), "expected a non-empty PHP_VERSION"), other => panic!("expected a string, got {other:?}"), } } + + #[test] + #[should_panic(expected = "is not reported by the platform payload")] + fn platform_info_rejects_an_unreported_constant() { + PlatformInfo::default().has_constant("SHIRABE_DOES_NOT_EXIST_XYZ", None); + } } diff --git a/crates/shirabe/src/command/base_dependency_command.rs b/crates/shirabe/src/command/base_dependency_command.rs index c4399f34..46cc654f 100644 --- a/crates/shirabe/src/command/base_dependency_command.rs +++ b/crates/shirabe/src/command/base_dependency_command.rs @@ -78,7 +78,7 @@ pub trait BaseDependencyCommand: BaseCommand { .map(|(k, v)| (k, PhpMixed::String(v))) .collect(); repos.push(crate::repository::RepositoryInterfaceHandle::new( - PlatformRepository::new(vec![], platform_overrides)?, + PlatformRepository::new(vec![], platform_overrides, None, None)?, )); } else { let repository_manager = composer.get_repository_manager().clone(); @@ -109,7 +109,7 @@ pub trait BaseDependencyCommand: BaseCommand { .into_iter() .collect(); repos.push(crate::repository::RepositoryInterfaceHandle::new( - PlatformRepository::new(vec![], platform_overrides)?, + PlatformRepository::new(vec![], platform_overrides, None, None)?, )); } diff --git a/crates/shirabe/src/command/check_platform_reqs_command.rs b/crates/shirabe/src/command/check_platform_reqs_command.rs index 903e0c4c..4e105e69 100644 --- a/crates/shirabe/src/command/check_platform_reqs_command.rs +++ b/crates/shirabe/src/command/check_platform_reqs_command.rs @@ -290,7 +290,7 @@ impl Command for CheckPlatformReqsCommand { requires_sorted.sort_by(|a, b| a.0.cmp(&b.0)); installed_repo.add_repository(crate::repository::RepositoryInterfaceHandle::new( - PlatformRepository::new(vec![], indexmap::IndexMap::new())?, + PlatformRepository::new(vec![], indexmap::IndexMap::new(), None, None)?, )); let installed_repo_with_platform = installed_repo; diff --git a/crates/shirabe/src/command/completion_trait.rs b/crates/shirabe/src/command/completion_trait.rs index bdd42dff..67ab23be 100644 --- a/crates/shirabe/src/command/completion_trait.rs +++ b/crates/shirabe/src/command/completion_trait.rs @@ -80,14 +80,14 @@ pub trait CompletionTrait: BaseCommand { .into_iter() .map(|(k, v)| (k, PhpMixed::String(v))) .collect(); - PlatformRepository::new(vec![], overrides)? + PlatformRepository::new(vec![], overrides, None, None)? } else { let platform_cfg = composer.get_config().borrow().get("platform"); let overrides: IndexMap<String, PhpMixed> = platform_cfg .as_array() .map(|m| m.iter().map(|(k, v)| (k.clone(), v.clone())).collect()) .unwrap_or_default(); - PlatformRepository::new(vec![], overrides)? + PlatformRepository::new(vec![], overrides, None, None)? }; if input.get_completion_value().is_empty() { // to reduce noise, when no text is yet entered we list only two entries for ext- and lib- prefixes @@ -286,7 +286,7 @@ pub trait CompletionTrait: BaseCommand { .as_array() .map(|m| m.iter().map(|(k, v)| (k.clone(), v.clone())).collect()) .unwrap_or_default(); - let mut repos = PlatformRepository::new(vec![], overrides)?; + let mut repos = PlatformRepository::new(vec![], overrides, None, None)?; let pattern = base_package::package_name_to_regexp(&format!("{}*", input.get_completion_value())); diff --git a/crates/shirabe/src/command/create_project_command.rs b/crates/shirabe/src/command/create_project_command.rs index dcf0e0f8..aa1b79a8 100644 --- a/crates/shirabe/src/command/create_project_command.rs +++ b/crates/shirabe/src/command/create_project_command.rs @@ -684,6 +684,8 @@ impl CreateProjectCommand { .collect(), _ => indexmap::IndexMap::new(), }, + None, + None, )?; // find the latest version if there are multiple diff --git a/crates/shirabe/src/command/diagnose_command.rs b/crates/shirabe/src/command/diagnose_command.rs index 8483eb56..65240e51 100644 --- a/crates/shirabe/src/command/diagnose_command.rs +++ b/crates/shirabe/src/command/diagnose_command.rs @@ -1223,7 +1223,7 @@ impl Command for DiagnoseCommand { let platform_overrides_unboxed: indexmap::IndexMap<String, PhpMixed> = platform_overrides.into_iter().collect(); let mut platform_repo = - PlatformRepository::new(vec![], platform_overrides_unboxed).unwrap(); + PlatformRepository::new(vec![], platform_overrides_unboxed, None, None).unwrap(); let php_pkg = <PlatformRepository as crate::repository::RepositoryInterface>::find_package( &mut platform_repo, "php", diff --git a/crates/shirabe/src/command/init_command.rs b/crates/shirabe/src/command/init_command.rs index f2bf19d7..5168b2ba 100644 --- a/crates/shirabe/src/command/init_command.rs +++ b/crates/shirabe/src/command/init_command.rs @@ -830,7 +830,7 @@ impl Command for InitCommand { let mut repos: Vec<crate::repository::RepositoryInterfaceHandle> = vec![crate::repository::RepositoryInterfaceHandle::new( - PlatformRepository::new(vec![], IndexMap::new())?, + PlatformRepository::new(vec![], IndexMap::new(), None, None)?, )]; let mut create_default_packagist_repo = true; for repo in &repositories { diff --git a/crates/shirabe/src/command/package_discovery_trait.rs b/crates/shirabe/src/command/package_discovery_trait.rs index 5c921e95..db7db488 100644 --- a/crates/shirabe/src/command/package_discovery_trait.rs +++ b/crates/shirabe/src/command/package_discovery_trait.rs @@ -42,7 +42,7 @@ pub trait PackageDiscoveryTrait: BaseCommand { // PHP: array_merge([new PlatformRepository], RepositoryFactory::defaultReposWithDefaultManager($this->getIO())) let mut repos: Vec<crate::repository::RepositoryInterfaceHandle> = vec![crate::repository::RepositoryInterfaceHandle::new( - PlatformRepository::new(vec![], IndexMap::new()) + PlatformRepository::new(vec![], IndexMap::new(), None, None) .expect("PlatformRepository::new should not fail"), )]; let io_owned: std::rc::Rc<std::cell::RefCell<dyn IOInterface>> = self.get_io(); diff --git a/crates/shirabe/src/command/require_command.rs b/crates/shirabe/src/command/require_command.rs index f4410c19..85e53a37 100644 --- a/crates/shirabe/src/command/require_command.rs +++ b/crates/shirabe/src/command/require_command.rs @@ -916,8 +916,12 @@ impl Command for RequireCommand { .map(|m| m.iter().map(|(k, v)| (k.clone(), v.clone())).collect()) .unwrap_or_default(); // initialize self.repos as it is used by the PackageDiscoveryTrait - let platform_repo = - PlatformRepositoryHandle::new(PlatformRepository::new(vec![], platform_overrides_map)?); + let platform_repo = PlatformRepositoryHandle::new(PlatformRepository::new( + vec![], + platform_overrides_map, + None, + None, + )?); let mut combined: Vec<crate::repository::RepositoryInterfaceHandle> = vec![platform_repo.clone().into()]; for repo in repos { diff --git a/crates/shirabe/src/command/search_command.rs b/crates/shirabe/src/command/search_command.rs index 0b675109..4bf75c8b 100644 --- a/crates/shirabe/src/command/search_command.rs +++ b/crates/shirabe/src/command/search_command.rs @@ -111,7 +111,7 @@ impl Command for SearchCommand { input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, ) -> anyhow::Result<i64> { - let platform_repo = PlatformRepository::new4(vec![], IndexMap::new(), None, None)?; + let platform_repo = PlatformRepository::new(vec![], IndexMap::new(), None, None)?; let io = self.get_io(); let format = input diff --git a/crates/shirabe/src/command/show_command.rs b/crates/shirabe/src/command/show_command.rs index 44733531..ff20f325 100644 --- a/crates/shirabe/src/command/show_command.rs +++ b/crates/shirabe/src/command/show_command.rs @@ -1784,8 +1784,12 @@ impl Command for ShowCommand { platform_overrides = p.into_iter().collect(); } } - let platform_repo = - PlatformRepositoryHandle::new(PlatformRepository::new(vec![], platform_overrides)?); + let platform_repo = PlatformRepositoryHandle::new(PlatformRepository::new( + vec![], + platform_overrides, + None, + None, + )?); let mut locked_repo: Option<RepositoryInterfaceHandle> = None; // The single-package $package binding from PHP gets surfaced here. diff --git a/crates/shirabe/src/command/suggests_command.rs b/crates/shirabe/src/command/suggests_command.rs index 5c73da9b..96989dc0 100644 --- a/crates/shirabe/src/command/suggests_command.rs +++ b/crates/shirabe/src/command/suggests_command.rs @@ -133,6 +133,8 @@ impl Command for SuggestsCommand { installed_repos.push(RepositoryInterfaceHandle::new(PlatformRepository::new( vec![], platform_overrides, + None, + None, )?)); let locked_repo = composer.get_locker().borrow_mut().get_locked_repository( !input @@ -151,6 +153,8 @@ impl Command for SuggestsCommand { installed_repos.push(RepositoryInterfaceHandle::new(PlatformRepository::new( vec![], platform_overrides, + None, + None, )?)); installed_repos.push( composer diff --git a/crates/shirabe/src/dependency_resolver/problem.rs b/crates/shirabe/src/dependency_resolver/problem.rs index 09adafea..8ee4b816 100644 --- a/crates/shirabe/src/dependency_resolver/problem.rs +++ b/crates/shirabe/src/dependency_resolver/problem.rs @@ -458,7 +458,7 @@ impl Problem { ); // Per-extension version info can't be known statically; query the real PHP - // runtime via the RPC bridge, same as platform::runtime::Runtime::get_extension_version. + // runtime via the RPC bridge, as PHP's Composer\Platform\Runtime does. let runtime_version = shirabe_php_rpc::phpversion(&ext); let effective_version = match runtime_version { None => "0".to_string(), diff --git a/crates/shirabe/src/installer.rs b/crates/shirabe/src/installer.rs index bda4e2f6..091fb35f 100644 --- a/crates/shirabe/src/installer.rs +++ b/crates/shirabe/src/installer.rs @@ -1323,6 +1323,8 @@ impl Installer { Ok(PlatformRepositoryHandle::new(PlatformRepository::new( vec![], platform_overrides, + None, + None, )?)) } diff --git a/crates/shirabe/src/platform.rs b/crates/shirabe/src/platform.rs index 6abbb769..11b00e3f 100644 --- a/crates/shirabe/src/platform.rs +++ b/crates/shirabe/src/platform.rs @@ -1,7 +1,9 @@ +//! `Composer\Platform\Runtime` has no Rust counterpart. Its work belongs to the running PHP +//! interpreter, so it is ported as PHP into the RPC worker (`ShirabePlatformRuntime` in +//! `shirabe-php-rpc`), and its callers read the answers off `shirabe_php_rpc::PlatformInfo`. + pub mod hhvm_detector; -pub mod runtime; pub mod version; pub use hhvm_detector::*; -pub use runtime::*; pub use version::*; diff --git a/crates/shirabe/src/platform/runtime.rs b/crates/shirabe/src/platform/runtime.rs deleted file mode 100644 index 265f007c..00000000 --- a/crates/shirabe/src/platform/runtime.rs +++ /dev/null @@ -1,237 +0,0 @@ -//! ref: composer/src/Composer/Platform/Runtime.php - -use indexmap::IndexMap; -use shirabe_external_packages::composer::pcre::{CaptureKey, Preg}; -use shirabe_php_rpc::{PhpThrow, PluginValue}; -use shirabe_php_shim::{ - PhpMixed, RuntimeException, function_exists, html_entity_decode, implode, ltrim, php_regex, - strip_tags, trim, -}; - -/// Seam over the PHP runtime so PlatformRepository can be tested against mocked -/// extension/constant/function probes. PHP has no such interface (the test mocks the -/// concrete `Composer\Platform\Runtime` directly); it is introduced here to keep the -/// consumer dependent only on trait methods. -pub trait RuntimeInterface: std::fmt::Debug { - fn has_constant(&self, constant_name: &str, class: Option<String>) -> bool; - fn get_constant(&self, constant_name: &str, class: Option<String>) -> PhpMixed; - /// `callable` carries the PHP callable spec (a function name string or a - /// `[class, method]` list), matching PHP `invoke($callable, $arguments)`. - fn invoke(&self, callable: PhpMixed, arguments: Vec<PhpMixed>) -> PhpMixed; - fn has_class(&self, class: &str) -> bool; - fn construct(&self, class: &str, arguments: Vec<PhpMixed>) -> anyhow::Result<PhpMixed>; - fn get_extensions(&self) -> Vec<String>; - fn get_extension_version(&self, extension: &str) -> String; - fn get_extension_info(&self, extension: &str) -> anyhow::Result<String>; -} - -#[derive(Debug)] -pub struct Runtime; - -impl RuntimeInterface for Runtime { - fn has_constant(&self, constant_name: &str, class: Option<String>) -> bool { - shirabe_php_rpc::has_constant(<rim( - &format!("{}::{}", class.as_deref().unwrap_or(""), constant_name), - Some(":"), - )) - } - - fn get_constant(&self, constant_name: &str, class: Option<String>) -> PhpMixed { - shirabe_php_rpc::get_constant(<rim( - &format!("{}::{}", class.as_deref().unwrap_or(""), constant_name), - Some(":"), - )) - } - - fn invoke(&self, callable: PhpMixed, arguments: Vec<PhpMixed>) -> PhpMixed { - // PHP: return $callable(...$arguments); - // Only the specific dynamic callables PlatformRepository actually reaches are - // wired through php-rpc; arbitrary PHP callables are still unsupported. - match (&callable, arguments.as_slice()) { - (PhpMixed::String(name), [PhpMixed::String(arg)]) if name == "inet_pton" => { - shirabe_php_rpc::inet_pton(arg) - } - (PhpMixed::String(name), []) if name == "curl_version" => { - let mut version = IndexMap::new(); - if let Some(v) = shirabe_php_rpc::curl_version() { - version.insert("version".to_string(), PhpMixed::String(v)); - } - PhpMixed::Array(version) - } - (PhpMixed::List(spec), _) => match class_callable(spec) { - ("ResourceBundle", "create") => resource_bundle_create(arguments), - ("IntlChar", "getUnicodeVersion") => { - php_value(shirabe_php_rpc::call_static_method( - "IntlChar", - "getUnicodeVersion", - Vec::new(), - None, - )) - } - (class, method) => panic!( - "the PHP callable `{class}::{method}` is not wired through the runtime seam" - ), - }, - _ => panic!("the PHP callable {callable:?} is not wired through the runtime seam"), - } - } - - fn has_class(&self, class: &str) -> bool { - shirabe_php_rpc::class_exists(class) - } - - fn construct(&self, class: &str, arguments: Vec<PhpMixed>) -> anyhow::Result<PhpMixed> { - match class { - "Imagick" => imagick_version(arguments), - other => Err(anyhow::anyhow!(RuntimeException { - message: format!("the PHP class `{other}` is not wired through the runtime seam"), - code: 0, - })), - } - } - - fn get_extensions(&self) -> Vec<String> { - shirabe_php_rpc::get_loaded_extensions() - } - - fn get_extension_version(&self, extension: &str) -> String { - shirabe_php_rpc::phpversion(extension).unwrap_or_else(|| "0".to_string()) - } - - fn get_extension_info(&self, extension: &str) -> anyhow::Result<String> { - Ok(shirabe_php_rpc::get_extension_info(extension)) - } -} - -/// The `[class, method]` pair of a PHP callable given in array form. -fn class_callable(spec: &[PhpMixed]) -> (&str, &str) { - match spec { - [PhpMixed::String(class), PhpMixed::String(method)] => (class, method), - other => panic!("a PHP callable given as an array must be [class, method], got {other:?}"), - } -} - -/// Unwraps an RPC outcome whose failure means the runtime probe itself is broken, not that the -/// probed extension is absent. -fn php_value(outcome: anyhow::Result<Result<PluginValue, PhpThrow>>) -> PhpMixed { - match outcome { - Ok(Ok(value)) => value - .to_php_mixed() - .expect("a runtime probe answers with plain values"), - Ok(Err(throw)) => panic!("the PHP runtime probe failed: {}", throw.message), - Err(e) => panic!("the PHP runtime probe could not be sent: {e:#}"), - } -} - -/// PHP `ResourceBundle::create(...)`, whose result the caller reads `->get('Version')` off. -/// A live PHP object has no `PhpMixed` counterpart, so that entry crosses in its place. -fn resource_bundle_create(arguments: Vec<PhpMixed>) -> PhpMixed { - let bundle = match php_handle(shirabe_php_rpc::call_static_method( - "ResourceBundle", - "create", - arguments.iter().map(PluginValue::from_php_mixed).collect(), - None, - )) { - Some(phandle) => phandle, - // PHP returns null when the bundle cannot be opened. - None => return PhpMixed::Null, - }; - let version = php_value(shirabe_php_rpc::call_php_method( - bundle, - "get", - vec![PluginValue::string("Version")], - None, - )); - let _ = shirabe_php_rpc::release_php_handle(bundle); - PhpMixed::Object(IndexMap::from([("Version".to_string(), version)])) -} - -/// PHP `(new Imagick())->getVersion()`, reported as the entries the caller reads. -fn imagick_version(arguments: Vec<PhpMixed>) -> anyhow::Result<PhpMixed> { - let imagick = php_handle(shirabe_php_rpc::new_object( - "Imagick", - arguments.iter().map(PluginValue::from_php_mixed).collect(), - None, - )) - .ok_or_else(|| { - anyhow::anyhow!(RuntimeException { - message: "`new Imagick` did not answer with an object".to_string(), - code: 0, - }) - })?; - let version = php_value(shirabe_php_rpc::call_php_method( - imagick, - "getVersion", - Vec::new(), - None, - )); - let _ = shirabe_php_rpc::release_php_handle(imagick); - Ok(version) -} - -/// The handle of a PHP-side object an RPC answered with, or `None` when it answered with null. -fn php_handle(outcome: anyhow::Result<Result<PluginValue, PhpThrow>>) -> Option<u64> { - match outcome { - Ok(Ok(PluginValue::PhpHandle(handle))) => Some(handle.phandle), - Ok(Ok(PluginValue::Null)) => None, - Ok(Ok(other)) => panic!("the PHP runtime probe answered with {other:?}, not an object"), - Ok(Err(throw)) => panic!("the PHP runtime probe failed: {}", throw.message), - Err(e) => panic!("the PHP runtime probe could not be sent: {e:#}"), - } -} - -impl Runtime { - pub fn has_function(&self, f: &str) -> bool { - function_exists(f) - } - - pub fn parse_html_extension_info(html: &str) -> String { - let mut result: Vec<String> = vec![]; - - let mut matches: IndexMap<CaptureKey, String> = IndexMap::new(); - if Preg::match3( - php_regex!(r"~<h2>\s*<a[^>]*>([^<]+)</a>\s*</h2>~i"), - html, - Some(&mut matches), - ) { - result.push(trim( - &html_entity_decode( - matches - .get(&CaptureKey::ByIndex(1)) - .map(|s| s.as_str()) - .unwrap_or(""), - ), - None, - )); - result.push(String::new()); - } - - let mut matches: IndexMap<CaptureKey, Vec<String>> = IndexMap::new(); - if Preg::match_all3( - php_regex!( - r#"~<tr>\s*<td class="e">\s*(.*?)\s*</td>\s*<td class="v">\s*(.*?)\s*</td>\s*</tr>~is"# - ), - html, - Some(&mut matches), - ) > 0 - { - let group1 = matches - .get(&CaptureKey::ByIndex(1)) - .cloned() - .unwrap_or_default(); - let group2 = matches - .get(&CaptureKey::ByIndex(2)) - .cloned() - .unwrap_or_default(); - let count = std::cmp::min(group1.len(), group2.len()); - - for i in 0..count { - let key = trim(&html_entity_decode(&strip_tags(&group1[i])), None); - let value = trim(&html_entity_decode(&strip_tags(&group2[i])), None); - result.push(format!("{} => {}", key, value)); - } - } - - implode("\n", &result) - } -} diff --git a/crates/shirabe/src/repository/platform_repository.rs b/crates/shirabe/src/repository/platform_repository.rs index 5b205ba0..944bb4db 100644 --- a/crates/shirabe/src/repository/platform_repository.rs +++ b/crates/shirabe/src/repository/platform_repository.rs @@ -11,16 +11,14 @@ use crate::package::PackageInterfaceHandle; use crate::package::version::VersionParser; use crate::platform::HhvmDetector; use crate::platform::HhvmDetectorInterface; -use crate::platform::Runtime; -use crate::platform::RuntimeInterface; use crate::platform::Version; use crate::plugin::plugin_interface::{self}; use crate::repository::ArrayRepository; use crate::repository::RepositoryInterface; -use crate::util::Silencer; use indexmap::IndexMap; use shirabe_external_packages::composer::pcre::{CaptureKey, Preg}; use shirabe_external_packages::composer::xdebug_handler::XdebugHandler; +use shirabe_php_rpc::PlatformInfo; use shirabe_php_shim::{ InvalidArgumentException, PhpMixed, UnexpectedValueException, array_map_str_fn, array_slice_strs, explode, get_class, implode, in_array_strict, is_string, php_regex, @@ -47,27 +45,21 @@ pub struct PlatformRepository { pub(crate) version_parser: Option<VersionParser>, pub(crate) overrides: IndexMap<String, PlatformOverride>, pub(crate) disabled_packages: IndexMap<String, CompletePackageInterfaceHandle>, - pub(crate) runtime: Box<dyn RuntimeInterface>, + pub(crate) platform_info: Option<PlatformInfo>, pub(crate) hhvm_detector: Box<dyn HhvmDetectorInterface>, } impl PlatformRepository { const PLATFORM_PACKAGE_REGEX: &'static str = "{^(?:php(?:-64bit|-ipv6|-zts|-debug)?|hhvm|(?:ext|lib)-[a-z0-9](?:[_.-]?[a-z0-9]+)*|composer(?:-(?:plugin|runtime)-api)?)$}iD"; + /// A `None` `platform_info` is resolved from the PHP worker on the first `initialize()`, so + /// constructing the repository on its own never starts the worker. pub fn new( packages: Vec<PackageInterfaceHandle>, overrides: IndexMap<String, PhpMixed>, - ) -> anyhow::Result<Self> { - Self::new4(packages, overrides, None, None) - } - - pub fn new4( - packages: Vec<PackageInterfaceHandle>, - overrides: IndexMap<String, PhpMixed>, - runtime: Option<Box<dyn RuntimeInterface>>, + platform_info: Option<PlatformInfo>, hhvm_detector: Option<Box<dyn HhvmDetectorInterface>>, ) -> anyhow::Result<Self> { - let runtime: Box<dyn RuntimeInterface> = runtime.unwrap_or_else(|| Box::new(Runtime)); let hhvm_detector: Box<dyn HhvmDetectorInterface> = hhvm_detector.unwrap_or_else(|| Box::new(HhvmDetector::new(None, None))); let mut overrides_map: IndexMap<String, PlatformOverride> = IndexMap::new(); @@ -107,7 +99,7 @@ impl PlatformRepository { version_parser: None, overrides: overrides_map, disabled_packages: IndexMap::new(), - runtime, + platform_info, hhvm_detector, }; for package in packages { @@ -139,6 +131,11 @@ impl PlatformRepository { pub(crate) fn initialize(&mut self) -> anyhow::Result<()> { self.inner.initialize(); + let platform_info = self + .platform_info + .get_or_insert_with(|| shirabe_php_rpc::get_platform_info().clone()) + .clone(); + let mut libraries: IndexMap<String, bool> = IndexMap::new(); self.version_parser = Some(VersionParser::new()); @@ -214,7 +211,7 @@ impl PlatformRepository { CompletePackageHandle::from_complete_package(composer_runtime_api).into(), )?; - let php_version_const = self.runtime.get_constant("PHP_VERSION", None); + let php_version_const = platform_info.get_constant("PHP_VERSION", None); let php_version_str = match &php_version_const { PhpMixed::String(s) => s.clone(), _ => "".to_string(), @@ -245,8 +242,7 @@ impl PlatformRepository { php.set_description("The PHP interpreter".to_string()); self.add_package(CompletePackageHandle::from_complete_package(php).into())?; - if self - .runtime + if platform_info .get_constant("PHP_DEBUG", None) .as_bool() .unwrap_or(false) @@ -260,9 +256,8 @@ impl PlatformRepository { self.add_package(CompletePackageHandle::from_complete_package(phpdebug).into())?; } - if self.runtime.has_constant("PHP_ZTS", None) - && self - .runtime + if platform_info.has_constant("PHP_ZTS", None) + && platform_info .get_constant("PHP_ZTS", None) .as_bool() .unwrap_or(false) @@ -276,8 +271,7 @@ impl PlatformRepository { self.add_package(CompletePackageHandle::from_complete_package(phpzts).into())?; } - if self - .runtime + if platform_info .get_constant("PHP_INT_SIZE", None) .as_int() .map(|v| v == 8) @@ -294,15 +288,8 @@ impl PlatformRepository { // The AF_INET6 constant is only defined if ext-sockets is available but // IPv6 support might still be available. - let has_inet6 = self.runtime.has_constant("AF_INET6", None); - // PHP: Silencer::call([$this->runtime, 'invoke'], 'inet_pton', ['::']) - let inet_pton_check = Silencer::call(|| { - Ok::<PhpMixed, anyhow::Error>(self.runtime.invoke( - PhpMixed::String("inet_pton".to_string()), - vec![PhpMixed::String("::".to_string())], - )) - }) - .unwrap_or(PhpMixed::Bool(false)); + let has_inet6 = platform_info.has_constant("AF_INET6", None); + let inet_pton_check = &platform_info.inet_pton_ipv6; if has_inet6 || !matches!(inet_pton_check, PhpMixed::Bool(false)) { let mut php_ipv6 = CompletePackage::new("php-ipv6".to_string(), version, pretty_version); @@ -310,7 +297,7 @@ impl PlatformRepository { self.add_package(CompletePackageHandle::from_complete_package(php_ipv6).into())?; } - let loaded_extensions = self.runtime.get_extensions(); + let loaded_extensions = platform_info.get_extensions().to_vec(); // Extensions scanning for name in &loaded_extensions { @@ -318,7 +305,7 @@ impl PlatformRepository { continue; } - self.add_extension(name, &self.runtime.get_extension_version(name))?; + self.add_extension(name, platform_info.get_extension_version(name))?; } // Check for Xdebug in a restarted process @@ -340,13 +327,13 @@ impl PlatformRepository { for name in &loaded_extensions { match name.as_str() { "amqp" => { - let info = self.runtime.get_extension_info(name)?; + let info = platform_info.get_extension_info(name); // librabbitmq version => 0.9.0 let mut librabbitmq_matches: IndexMap<CaptureKey, String> = IndexMap::new(); if Preg::is_match3( php_regex!("/^librabbitmq version => (?<version>.+)$/im"), - &info, + info, Some(&mut librabbitmq_matches), ) { self.add_library( @@ -365,7 +352,7 @@ impl PlatformRepository { let mut protocol_matches: IndexMap<CaptureKey, String> = IndexMap::new(); if Preg::is_match3( php_regex!("/^AMQP protocol version => (?<version>.+)$/im"), - &info, + info, Some(&mut protocol_matches), ) { let version_str = protocol_matches @@ -384,13 +371,13 @@ impl PlatformRepository { } "bz2" => { - let info = self.runtime.get_extension_info(name)?; + let info = platform_info.get_extension_info(name); // BZip2 Version => 1.0.6, 6-Sept-2010 let mut matches: IndexMap<CaptureKey, String> = IndexMap::new(); if Preg::is_match3( php_regex!("/^BZip2 Version => (?<version>.*),/im"), - &info, + info, Some(&mut matches), ) { self.add_library( @@ -407,9 +394,7 @@ impl PlatformRepository { } "curl" => { - let curl_version = self - .runtime - .invoke(PhpMixed::String("curl_version".to_string()), vec![]); + let curl_version = &platform_info.curl_version; let curl_version_str = curl_version .as_array() .and_then(|m| m.get("version")) @@ -419,13 +404,13 @@ impl PlatformRepository { self.add_library(&mut libraries, name, Some(cv), None, &[], &[])?; } - let info = self.runtime.get_extension_info(name)?; + let info = platform_info.get_extension_info(name); // SSL Version => OpenSSL/1.0.1t let mut ssl_matches: IndexMap<CaptureKey, String> = IndexMap::new(); if Preg::is_match3( php_regex!("{^SSL Version => (?<library>[^/]+)/(?<version>.+)$}im"), - &info, + info, Some(&mut ssl_matches), ) { let ssl_library_raw = ssl_matches @@ -495,7 +480,7 @@ impl PlatformRepository { php_regex!( "{^libSSH Version => (?<library>[^/]+)/(?<version>.+?)(?:/.*)?$}im" ), - &info, + info, Some(&mut ssh_matches), ) { let ssh_library = ssh_matches @@ -520,7 +505,7 @@ impl PlatformRepository { let mut zlib_matches: IndexMap<CaptureKey, String> = IndexMap::new(); if Preg::is_match3( php_regex!("{^ZLib Version => (?<version>.+)$}im"), - &info, + info, Some(&mut zlib_matches), ) { self.add_library( @@ -537,13 +522,13 @@ impl PlatformRepository { } "date" => { - let info = self.runtime.get_extension_info(name)?; + let info = platform_info.get_extension_info(name); // timelib version => 2018.03 let mut timelib_matches: IndexMap<CaptureKey, String> = IndexMap::new(); if Preg::is_match3( php_regex!("/^timelib version => (?<version>.+)$/im"), - &info, + info, Some(&mut timelib_matches), ) { self.add_library( @@ -562,7 +547,7 @@ impl PlatformRepository { let mut zoneinfo_source_matches: IndexMap<CaptureKey, String> = IndexMap::new(); if Preg::is_match3( php_regex!("/^Timezone Database => (?<source>internal|external)$/im"), - &info, + info, Some(&mut zoneinfo_source_matches), ) { let external = zoneinfo_source_matches @@ -574,7 +559,7 @@ impl PlatformRepository { php_regex!( "/^\"Olson\" Timezone Database Version => (?<version>.+?)(?:\\.system)?$/im" ), - &info, + info, Some(&mut zoneinfo_matches), ) { let zoneinfo_version = zoneinfo_matches @@ -608,13 +593,13 @@ impl PlatformRepository { } "fileinfo" => { - let info = self.runtime.get_extension_info(name)?; + let info = platform_info.get_extension_info(name); // libmagic => 537 let mut magic_matches: IndexMap<CaptureKey, String> = IndexMap::new(); if Preg::is_match3( php_regex!("/^libmagic => (?<version>.+)$/im"), - &info, + info, Some(&mut magic_matches), ) { self.add_library( @@ -631,7 +616,7 @@ impl PlatformRepository { } "gd" => { - let gd_version = self.runtime.get_constant("GD_VERSION", None); + let gd_version = platform_info.get_constant("GD_VERSION", None); let gd_version_str = match &gd_version { PhpMixed::String(s) => Some(s.clone()), _ => None, @@ -645,12 +630,12 @@ impl PlatformRepository { &[], )?; - let info = self.runtime.get_extension_info(name)?; + let info = platform_info.get_extension_info(name); let mut libjpeg_matches: IndexMap<CaptureKey, String> = IndexMap::new(); if Preg::is_match3( php_regex!("/^libJPEG Version => (?<version>.+?)(?: compatible)?$/im"), - &info, + info, Some(&mut libjpeg_matches), ) { let libjpeg_version = libjpeg_matches @@ -671,7 +656,7 @@ impl PlatformRepository { let mut libpng_matches: IndexMap<CaptureKey, String> = IndexMap::new(); if Preg::is_match3( php_regex!("/^libPNG Version => (?<version>.+)$/im"), - &info, + info, Some(&mut libpng_matches), ) { self.add_library( @@ -689,7 +674,7 @@ impl PlatformRepository { let mut freetype_matches: IndexMap<CaptureKey, String> = IndexMap::new(); if Preg::is_match3( php_regex!("/^FreeType Version => (?<version>.+)$/im"), - &info, + info, Some(&mut freetype_matches), ) { self.add_library( @@ -707,7 +692,7 @@ impl PlatformRepository { let mut libxpm_matches: IndexMap<CaptureKey, String> = IndexMap::new(); if Preg::is_match3( php_regex!("/^libXpm Version => (?<versionId>\\d+)$/im"), - &info, + info, Some(&mut libxpm_matches), ) { let version_id: i64 = libxpm_matches @@ -727,7 +712,7 @@ impl PlatformRepository { } "gmp" => { - let gmp_version = self.runtime.get_constant("GMP_VERSION", None); + let gmp_version = platform_info.get_constant("GMP_VERSION", None); let gmp_version_str = match &gmp_version { PhpMixed::String(s) => Some(s.clone()), _ => None, @@ -743,7 +728,7 @@ impl PlatformRepository { } "iconv" => { - let iconv_version = self.runtime.get_constant("ICONV_VERSION", None); + let iconv_version = platform_info.get_constant("ICONV_VERSION", None); let iconv_version_str = match &iconv_version { PhpMixed::String(s) => Some(s.clone()), _ => None, @@ -759,12 +744,12 @@ impl PlatformRepository { } "intl" => { - let info = self.runtime.get_extension_info(name)?; + let info = platform_info.get_extension_info(name); let description = "The ICU unicode and globalization support library"; // Truthy check is for testing only so we can make the condition fail - if self.runtime.has_constant("INTL_ICU_VERSION", None) { - let intl_icu_version = self.runtime.get_constant("INTL_ICU_VERSION", None); + if platform_info.has_constant("INTL_ICU_VERSION", None) { + let intl_icu_version = platform_info.get_constant("INTL_ICU_VERSION", None); let intl_icu_str = match &intl_icu_version { PhpMixed::String(s) => Some(s.clone()), _ => None, @@ -781,7 +766,7 @@ impl PlatformRepository { let mut matches: IndexMap<CaptureKey, String> = IndexMap::new(); if Preg::is_match3( php_regex!("/^ICU version => (?<version>.+)$/im"), - &info, + info, Some(&mut matches), ) { self.add_library( @@ -801,7 +786,7 @@ impl PlatformRepository { let mut zoneinfo_matches: IndexMap<CaptureKey, String> = IndexMap::new(); if Preg::is_match3( php_regex!("/^ICU TZData version => (?<version>.*)$/im"), - &info, + info, Some(&mut zoneinfo_matches), ) { let zi_version = zoneinfo_matches @@ -821,22 +806,11 @@ impl PlatformRepository { } // Add a separate version for the CLDR library version - if self.runtime.has_class("ResourceBundle") { - let resource_bundle = self.runtime.invoke( - PhpMixed::List(vec![ - PhpMixed::String("ResourceBundle".to_string()), - PhpMixed::String("create".to_string()), - ]), - vec![ - PhpMixed::String("root".to_string()), - PhpMixed::String("ICUDATA".to_string()), - PhpMixed::Bool(false), - ], - ); + if platform_info.has_class("ResourceBundle") { + let resource_bundle = &platform_info.resource_bundle; if !matches!(resource_bundle, PhpMixed::Null) { - // TODO(plugin): `$resourceBundle->get('Version')` dynamic method call let version_value = - Self::resource_bundle_get(&resource_bundle, "Version"); + Self::resource_bundle_get(resource_bundle, "Version"); let version_str = match version_value { PhpMixed::String(s) => Some(s), _ => None, @@ -852,16 +826,10 @@ impl PlatformRepository { } } - if self.runtime.has_class("IntlChar") { - let intl_char_versions = self.runtime.invoke( - PhpMixed::List(vec![ - PhpMixed::String("IntlChar".to_string()), - PhpMixed::String("getUnicodeVersion".to_string()), - ]), - vec![], - ); + if platform_info.has_class("IntlChar") { + let intl_char_versions = &platform_info.intl_char_unicode_version; let sliced = - shirabe_php_shim::array_slice_mixed(&intl_char_versions, 0, Some(3)); + shirabe_php_shim::array_slice_mixed(intl_char_versions, 0, Some(3)); let joined = implode(".", &Self::php_array_to_string_vec(&sliced)); self.add_library( &mut libraries, @@ -875,10 +843,9 @@ impl PlatformRepository { } "imagick" => { - let image_magick_version = self.runtime.construct("Imagick", Vec::new())?; - // TODO(plugin): `->getVersion()` is a dynamic method call on Imagick + let image_magick_version = &platform_info.imagick; let image_magick_version_str = - Self::imagick_get_version_string(&image_magick_version); + Self::imagick_get_version_string(image_magick_version); // 6.x: ImageMagick 6.2.9 08/24/06 Q16 http://www.imagemagick.org // 7.x: ImageMagick 7.0.8-34 Q16 x86_64 2019-03-23 https://imagemagick.org let mut matches: IndexMap<CaptureKey, String> = IndexMap::new(); @@ -907,17 +874,17 @@ impl PlatformRepository { } "ldap" => { - let info = self.runtime.get_extension_info(name)?; + let info = platform_info.get_extension_info(name); let mut matches: IndexMap<CaptureKey, String> = IndexMap::new(); let mut vendor_matches: IndexMap<CaptureKey, String> = IndexMap::new(); if Preg::is_match3( php_regex!("/^Vendor Version => (?<versionId>\\d+)$/im"), - &info, + info, Some(&mut matches), ) && Preg::is_match3( php_regex!("/^Vendor Name => (?<vendor>.+)$/im"), - &info, + info, Some(&mut vendor_matches), ) { let version_id: i64 = matches @@ -950,7 +917,7 @@ impl PlatformRepository { .collect(); let libxml_provides: Vec<String> = array_map_str_fn(|extension| format!("{}-libxml", extension), &intersected); - let libxml_dotted = self.runtime.get_constant("LIBXML_DOTTED_VERSION", None); + let libxml_dotted = platform_info.get_constant("LIBXML_DOTTED_VERSION", None); let libxml_dotted_str = match &libxml_dotted { PhpMixed::String(s) => Some(s.clone()), _ => None, @@ -966,13 +933,13 @@ impl PlatformRepository { } "mbstring" => { - let info = self.runtime.get_extension_info(name)?; + let info = platform_info.get_extension_info(name); // libmbfl version => 1.3.2 let mut libmbfl_matches: IndexMap<CaptureKey, String> = IndexMap::new(); if Preg::is_match3( php_regex!("/^libmbfl version => (?<version>.+)$/im"), - &info, + info, Some(&mut libmbfl_matches), ) { self.add_library( @@ -987,8 +954,8 @@ impl PlatformRepository { )?; } - if self.runtime.has_constant("MB_ONIGURUMA_VERSION", None) { - let oniguruma = self.runtime.get_constant("MB_ONIGURUMA_VERSION", None); + if platform_info.has_constant("MB_ONIGURUMA_VERSION", None) { + let oniguruma = platform_info.get_constant("MB_ONIGURUMA_VERSION", None); let oniguruma_str = match &oniguruma { PhpMixed::String(s) => Some(s.clone()), _ => None, @@ -1010,7 +977,7 @@ impl PlatformRepository { php_regex!( "/^(?:oniguruma|Multibyte regex \\(oniguruma\\)) version => (?<version>.+)$/im" ), - &info, + info, Some(&mut oniguruma_matches), ) { self.add_library( @@ -1028,13 +995,13 @@ impl PlatformRepository { } "memcached" => { - let info = self.runtime.get_extension_info(name)?; + let info = platform_info.get_extension_info(name); // libmemcached version => 1.0.18 let mut matches: IndexMap<CaptureKey, String> = IndexMap::new(); if Preg::is_match3( php_regex!("/^libmemcached version => (?<version>.+)$/im"), - &info, + info, Some(&mut matches), ) { self.add_library( @@ -1051,7 +1018,7 @@ impl PlatformRepository { } "openssl" => { - let openssl_text = self.runtime.get_constant("OPENSSL_VERSION_TEXT", None); + let openssl_text = platform_info.get_constant("OPENSSL_VERSION_TEXT", None); let openssl_text_str = match &openssl_text { PhpMixed::String(s) => s.clone(), _ => "".to_string(), @@ -1086,7 +1053,7 @@ impl PlatformRepository { } "pcre" => { - let pcre_version = self.runtime.get_constant("PCRE_VERSION", None); + let pcre_version = platform_info.get_constant("PCRE_VERSION", None); let pcre_version_str = match &pcre_version { PhpMixed::String(s) => s.clone(), _ => "".to_string(), @@ -1095,13 +1062,13 @@ impl PlatformRepository { Preg::replace(php_regex!("{^(\\S+).*}"), "$1", &pcre_version_str); self.add_library(&mut libraries, name, Some(&stripped), None, &[], &[])?; - let info = self.runtime.get_extension_info(name)?; + let info = platform_info.get_extension_info(name); // PCRE Unicode Version => 12.1.0 let mut pcre_unicode_matches: IndexMap<CaptureKey, String> = IndexMap::new(); if Preg::is_match3( php_regex!("/^PCRE Unicode Version => (?<version>.+)$/im"), - &info, + info, Some(&mut pcre_unicode_matches), ) { self.add_library( @@ -1118,14 +1085,14 @@ impl PlatformRepository { } "mysqlnd" | "pdo_mysql" => { - let info = self.runtime.get_extension_info(name)?; + let info = platform_info.get_extension_info(name); let mut matches: IndexMap<CaptureKey, String> = IndexMap::new(); if Preg::is_match3( php_regex!( "/^(?:Client API version|Version) => mysqlnd (?<version>.+?) /mi" ), - &info, + info, Some(&mut matches), ) { self.add_library( @@ -1142,12 +1109,12 @@ impl PlatformRepository { } "mongodb" => { - let info = self.runtime.get_extension_info(name)?; + let info = platform_info.get_extension_info(name); let mut libmongoc_matches: IndexMap<CaptureKey, String> = IndexMap::new(); if Preg::is_match3( php_regex!("/^libmongoc bundled version => (?<version>.+)$/im"), - &info, + info, Some(&mut libmongoc_matches), ) { self.add_library( @@ -1165,7 +1132,7 @@ impl PlatformRepository { let mut libbson_matches: IndexMap<CaptureKey, String> = IndexMap::new(); if Preg::is_match3( php_regex!("/^libbson bundled version => (?<version>.+)$/im"), - &info, + info, Some(&mut libbson_matches), ) { self.add_library( @@ -1182,8 +1149,8 @@ impl PlatformRepository { } "pgsql" => { - if self.runtime.has_constant("PGSQL_LIBPQ_VERSION", None) { - let pq_version = self.runtime.get_constant("PGSQL_LIBPQ_VERSION", None); + if platform_info.has_constant("PGSQL_LIBPQ_VERSION", None) { + let pq_version = platform_info.get_constant("PGSQL_LIBPQ_VERSION", None); let pq_version_str = match &pq_version { PhpMixed::String(s) => Some(s.clone()), _ => None, @@ -1198,12 +1165,12 @@ impl PlatformRepository { )?; } else { // intentional fall-through to next case... - let info = self.runtime.get_extension_info(name)?; + let info = platform_info.get_extension_info(name); let mut matches: IndexMap<CaptureKey, String> = IndexMap::new(); if Preg::is_match3( php_regex!("/^PostgreSQL\\(libpq\\) Version => (?<version>.*)$/im"), - &info, + info, Some(&mut matches), ) { self.add_library( @@ -1221,12 +1188,12 @@ impl PlatformRepository { } "pdo_pgsql" => { - let info = self.runtime.get_extension_info(name)?; + let info = platform_info.get_extension_info(name); let mut matches: IndexMap<CaptureKey, String> = IndexMap::new(); if Preg::is_match3( php_regex!("/^PostgreSQL\\(libpq\\) Version => (?<version>.*)$/im"), - &info, + info, Some(&mut matches), ) { self.add_library( @@ -1243,14 +1210,14 @@ impl PlatformRepository { } "pq" => { - let info = self.runtime.get_extension_info(name)?; + let info = platform_info.get_extension_info(name); // Used Library => Compiled => Linked // libpq => 14.3 (Ubuntu 14.3-1.pgdg22.04+1) => 15.0.2 let mut matches: IndexMap<CaptureKey, String> = IndexMap::new(); if Preg::is_match3( php_regex!("/^libpq => (?<compiled>.+) => (?<linked>.+)$/im"), - &info, + info, Some(&mut matches), ) { self.add_library( @@ -1267,7 +1234,7 @@ impl PlatformRepository { } "rdkafka" => { - if self.runtime.has_constant("RD_KAFKA_VERSION", None) { + if platform_info.has_constant("RD_KAFKA_VERSION", None) { // Interpreted as hex MM.mm.rr.xx: // - MM = Major // - mm = minor @@ -1275,8 +1242,7 @@ impl PlatformRepository { // - xx = pre-release id (0xff is the final release) // // pre-release ID in practice is always 0xff even for RCs etc, so we ignore it - let lib_rd_kafka_version_int = self - .runtime + let lib_rd_kafka_version_int = platform_info .get_constant("RD_KAFKA_VERSION", None) .as_int() .unwrap_or(0); @@ -1298,8 +1264,8 @@ impl PlatformRepository { } "libsodium" | "sodium" => { - if self.runtime.has_constant("SODIUM_LIBRARY_VERSION", None) { - let sodium = self.runtime.get_constant("SODIUM_LIBRARY_VERSION", None); + if platform_info.has_constant("SODIUM_LIBRARY_VERSION", None) { + let sodium = platform_info.get_constant("SODIUM_LIBRARY_VERSION", None); let sodium_str = match &sodium { PhpMixed::String(s) => Some(s.clone()), _ => None, @@ -1324,12 +1290,12 @@ impl PlatformRepository { } "sqlite3" | "pdo_sqlite" => { - let info = self.runtime.get_extension_info(name)?; + let info = platform_info.get_extension_info(name); let mut matches: IndexMap<CaptureKey, String> = IndexMap::new(); if Preg::is_match3( php_regex!("/^SQLite Library => (?<version>.+)$/im"), - &info, + info, Some(&mut matches), ) { self.add_library( @@ -1346,12 +1312,12 @@ impl PlatformRepository { } "ssh2" => { - let info = self.runtime.get_extension_info(name)?; + let info = platform_info.get_extension_info(name); let mut matches: IndexMap<CaptureKey, String> = IndexMap::new(); if Preg::is_match3( php_regex!("/^libssh2 version => (?<version>.+)$/im"), - &info, + info, Some(&mut matches), ) { self.add_library( @@ -1368,7 +1334,8 @@ impl PlatformRepository { } "xsl" => { - let libxslt_version = self.runtime.get_constant("LIBXSLT_DOTTED_VERSION", None); + let libxslt_version = + platform_info.get_constant("LIBXSLT_DOTTED_VERSION", None); let libxslt_str = match &libxslt_version { PhpMixed::String(s) => Some(s.clone()), _ => None, @@ -1382,13 +1349,13 @@ impl PlatformRepository { &[], )?; - let info = self.runtime.get_extension_info("xsl")?; + let info = platform_info.get_extension_info("xsl"); let mut matches: IndexMap<CaptureKey, String> = IndexMap::new(); if Preg::is_match3( php_regex!( "/^libxslt compiled against libxml Version => (?<version>.+)$/im" ), - &info, + info, Some(&mut matches), ) { self.add_library( @@ -1405,12 +1372,12 @@ impl PlatformRepository { } "yaml" => { - let info = self.runtime.get_extension_info("yaml")?; + let info = platform_info.get_extension_info("yaml"); let mut matches: IndexMap<CaptureKey, String> = IndexMap::new(); if Preg::is_match3( php_regex!("/^LibYAML Version => (?<version>.+)$/im"), - &info, + info, Some(&mut matches), ) { self.add_library( @@ -1427,13 +1394,9 @@ impl PlatformRepository { } "zip" => { - if self - .runtime - .has_constant("LIBZIP_VERSION", Some("ZipArchive".to_string())) - { - let libzip = self - .runtime - .get_constant("LIBZIP_VERSION", Some("ZipArchive".to_string())); + if platform_info.has_constant("LIBZIP_VERSION", Some("ZipArchive")) { + let libzip = + platform_info.get_constant("LIBZIP_VERSION", Some("ZipArchive")); let libzip_str = match &libzip { PhpMixed::String(s) => Some(s.clone()), _ => None, @@ -1450,8 +1413,8 @@ impl PlatformRepository { } "zlib" => { - if self.runtime.has_constant("ZLIB_VERSION", None) { - let zlib = self.runtime.get_constant("ZLIB_VERSION", None); + if platform_info.has_constant("ZLIB_VERSION", None) { + let zlib = platform_info.get_constant("ZLIB_VERSION", None); let zlib_str = match &zlib { PhpMixed::String(s) => Some(s.clone()), _ => None, @@ -1467,11 +1430,11 @@ impl PlatformRepository { // Linked Version => 1.2.8 } else { - let info = self.runtime.get_extension_info(name)?; + let info = platform_info.get_extension_info(name); let mut matches: IndexMap<CaptureKey, String> = IndexMap::new(); if Preg::is_match3( php_regex!("/^Linked Version => (?<version>.+)$/im"), - &info, + info, Some(&mut matches), ) { self.add_library( @@ -1840,7 +1803,7 @@ impl PlatformRepository { } /// PHP `$resourceBundle->get($key)`. A live PHP object has no `PhpMixed` counterpart, so - /// [`RuntimeInterface`] answers with the entries the caller reads instead of the object. + /// [`PlatformInfo`] carries the entries the caller reads instead of the object. fn resource_bundle_get(value: &PhpMixed, key: &str) -> PhpMixed { Self::php_object_field(value, key).unwrap_or(PhpMixed::Null) } diff --git a/crates/shirabe/tests/package/version/version_selector_test.rs b/crates/shirabe/tests/package/version/version_selector_test.rs index 13544399..16ad5fe2 100644 --- a/crates/shirabe/tests/package/version/version_selector_test.rs +++ b/crates/shirabe/tests/package/version/version_selector_test.rs @@ -120,7 +120,7 @@ fn test_latest_version_is_returned_that_matches_php_requirements() { let mut overrides: IndexMap<String, PhpMixed> = IndexMap::new(); overrides.insert("php".to_string(), PhpMixed::String("5.5.0".to_string())); - let mut platform = PlatformRepository::new(vec![], overrides).unwrap(); + let mut platform = PlatformRepository::new(vec![], overrides, None, None).unwrap(); let package0 = get_package("foo/bar", "0.9.0"); package0.__set_requires(IndexMap::from([( @@ -216,7 +216,7 @@ fn test_latest_version_is_returned_that_matches_ext_requirements() { let mut overrides: IndexMap<String, PhpMixed> = IndexMap::new(); overrides.insert("ext-zip".to_string(), PhpMixed::String("5.3.0".to_string())); - let mut platform = PlatformRepository::new(vec![], overrides).unwrap(); + let mut platform = PlatformRepository::new(vec![], overrides, None, None).unwrap(); let package1 = get_package("foo/bar", "1.0.0"); package1.__set_requires(IndexMap::from([( @@ -263,7 +263,7 @@ fn test_latest_version_is_returned_that_matches_ext_requirements() { fn test_latest_version_is_returned_that_matches_platform_ext() { let package_name = "foo/bar"; - let mut platform = PlatformRepository::new(vec![], IndexMap::new()).unwrap(); + let mut platform = PlatformRepository::new(vec![], IndexMap::new(), None, None).unwrap(); let package1 = get_package("foo/bar", "1.0.0"); let package2 = get_package("foo/bar", "2.0.0"); @@ -311,7 +311,7 @@ fn test_latest_version_is_returned_that_matches_composer_requirements() { "composer-runtime-api".to_string(), PhpMixed::String("1.0.0".to_string()), ); - let mut platform = PlatformRepository::new(vec![], overrides).unwrap(); + let mut platform = PlatformRepository::new(vec![], overrides, None, None).unwrap(); let package1 = get_package("foo/bar", "1.0.0"); package1.__set_requires(IndexMap::from([( diff --git a/crates/shirabe/tests/platform/main.rs b/crates/shirabe/tests/platform/main.rs index 936049ac..3cf2cf48 100644 --- a/crates/shirabe/tests/platform/main.rs +++ b/crates/shirabe/tests/platform/main.rs @@ -1,3 +1,2 @@ mod hhvm_detector_test; -mod runtime_test; mod version_test; diff --git a/crates/shirabe/tests/platform/runtime_test.rs b/crates/shirabe/tests/platform/runtime_test.rs deleted file mode 100644 index ebdeaffa..00000000 --- a/crates/shirabe/tests/platform/runtime_test.rs +++ /dev/null @@ -1,28 +0,0 @@ -//! ref: composer/tests/Composer/Test/Platform/RuntimeTest.php - -use shirabe::platform::runtime::Runtime; - -#[test] -fn test_parse_extension_info() { - for (html_input, expected_output) in provide_extension_infos() { - assert_eq!( - expected_output, - Runtime::parse_html_extension_info(html_input) - ); - } -} - -fn provide_extension_infos() -> Vec<(&'static str, &'static str)> { - vec![( - // 'pdo_sqlite' - "<h2><a name=\"module_pdo_sqlite\" href=\"#module_pdo_sqlite\">pdo_sqlite</a></h2> -<table> -<tr><td class=\"e\">PDO Driver for SQLite 3.x </td><td class=\"v\">enabled </td></tr> -<tr><td class=\"e\">SQLite Library </td><td class=\"v\">3.40.1 </td></tr> -</table>", - "pdo_sqlite - -PDO Driver for SQLite 3.x => enabled -SQLite Library => 3.40.1", - )] -} diff --git a/crates/shirabe/tests/repository/platform_repository_test.rs b/crates/shirabe/tests/repository/platform_repository_test.rs index 3a4bf45d..573f96ed 100644 --- a/crates/shirabe/tests/repository/platform_repository_test.rs +++ b/crates/shirabe/tests/repository/platform_repository_test.rs @@ -1,31 +1,15 @@ //! ref: composer/tests/Composer/Test/Repository/PlatformRepositoryTest.php use indexmap::IndexMap; -use mockall::predicate::eq; use shirabe::package::{BasePackageHandle, Link}; -use shirabe::platform::{HhvmDetectorInterface, RuntimeInterface}; +use shirabe::platform::HhvmDetectorInterface; use shirabe::repository::{ FindPackageConstraint, PlatformRepository, RepositoryInterface, SEARCH_NAME, }; +use shirabe_php_rpc::PlatformInfo; use shirabe_php_shim::PhpMixed; use shirabe_semver::constraint::SimpleConstraint; -// The Runtime/HhvmDetector seams are concrete structs in PHP; the tests mock them -// directly. -mockall::mock! { - pub Runtime {} - impl RuntimeInterface for Runtime { - fn has_constant(&self, constant_name: &str, class: Option<String>) -> bool; - fn get_constant(&self, constant_name: &str, class: Option<String>) -> PhpMixed; - fn invoke(&self, callable: PhpMixed, arguments: Vec<PhpMixed>) -> PhpMixed; - fn has_class(&self, class: &str) -> bool; - fn construct(&self, class: &str, arguments: Vec<PhpMixed>) -> anyhow::Result<PhpMixed>; - fn get_extensions(&self) -> Vec<String>; - fn get_extension_version(&self, extension: &str) -> String; - fn get_extension_info(&self, extension: &str) -> anyhow::Result<String>; - } -} - mockall::mock! { pub HhvmDetector {} impl HhvmDetectorInterface for HhvmDetector { @@ -34,25 +18,71 @@ mockall::mock! { } } -// The seam traits require `Debug` (so `PlatformRepository` can derive it); mockall does +// The seam trait requires `Debug` (so `PlatformRepository` can derive it); mockall does // not generate it for mocks. -impl std::fmt::Debug for MockRuntime { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str("MockRuntime") - } -} - impl std::fmt::Debug for MockHhvmDetector { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str("MockHhvmDetector") } } -/// PHP: ltrim($class.'::'.$constant, ':') -fn constant_key(constant_name: &str, class: Option<&str>) -> String { - format!("{}::{}", class.unwrap_or(""), constant_name) - .trim_start_matches(':') - .to_string() +/// The payload form of what a dataset's `Runtime` mock answers: the listed constants and classes +/// exist, and each extension is loaded at `extension_version` with `info` as its info() output. +fn platform_info( + constants: Vec<(String, Option<String>, PhpMixed)>, + extensions: Vec<String>, + extension_version: &str, + info: Option<&str>, + functions: &[(PhpMixed, Vec<PhpMixed>, PhpMixed)], + class_definitions: &[ClassDef], +) -> PlatformInfo { + let mut platform_info = PlatformInfo::default(); + + for (constant_name, class, value) in constants { + platform_info.__set_constant(&constant_name, class.as_deref(), value); + } + + for extension in &extensions { + platform_info.__set_extension_info(extension, info.unwrap_or_default()); + } + platform_info.__set_extensions(extensions, extension_version); + + for (callable, arguments, result) in functions { + match (callable, arguments.as_slice()) { + (PhpMixed::String(name), []) if name == "curl_version" => { + platform_info.curl_version = result.clone(); + } + (PhpMixed::String(name), [PhpMixed::String(address)]) + if name == "inet_pton" && address == "::" => + { + platform_info.inet_pton_ipv6 = result.clone(); + } + (PhpMixed::List(spec), _) => match spec.as_slice() { + [PhpMixed::String(class), PhpMixed::String(method)] + if class == "ResourceBundle" && method == "create" => + { + platform_info.resource_bundle = result.clone(); + } + [PhpMixed::String(class), PhpMixed::String(method)] + if class == "IntlChar" && method == "getUnicodeVersion" => + { + platform_info.intl_char_unicode_version = result.clone(); + } + other => panic!("the platform payload does not report {other:?}"), + }, + other => panic!("the platform payload does not report {other:?}"), + } + } + + for definition in class_definitions { + match (definition.class, &definition.construct) { + ("Imagick", Some((_arguments, result))) => platform_info.imagick = result.clone(), + (class, None) => platform_info.__set_class(class), + (class, Some(_)) => panic!("the platform payload does not construct {class}"), + } + } + + platform_info } #[test] @@ -64,7 +94,7 @@ fn test_hhvm_package() { .returning(|| Some("2.1.0".to_string())); let mut platform_repository = - PlatformRepository::new4(vec![], IndexMap::new(), None, Some(Box::new(hhvm_detector))) + PlatformRepository::new(vec![], IndexMap::new(), None, Some(Box::new(hhvm_detector))) .unwrap(); let hhvm = platform_repository @@ -137,44 +167,20 @@ fn php_flavor_test_cases() -> Vec<( #[test] fn test_php_version() { for (constants, packages, functions) in php_flavor_test_cases() { - let constants_has = constants.clone(); - let constants_get = constants.clone(); - - let mut runtime = MockRuntime::new(); - runtime - .expect_get_extensions() - .times(..) - .returning(Vec::new); - runtime - .expect_has_constant() - .times(..) - .returning(move |constant, class| { - constants_has.contains_key(&constant_key(constant, class.as_deref())) - }); - runtime - .expect_get_constant() - .times(..) - .returning(move |constant, class| { - constants_get - .get(&constant_key(constant, class.as_deref())) - .cloned() - .unwrap_or(PhpMixed::Null) - }); - runtime - .expect_invoke() - .times(..) - .returning(move |callable, arguments| { - for (c, a, ret) in &functions { - if *c == callable && *a == arguments { - return ret.clone(); - } - } - PhpMixed::Null - }); + let platform_info = platform_info( + constants + .into_iter() + .map(|(constant_name, value)| (constant_name, None, value)) + .collect(), + Vec::new(), + "", + None, + &functions, + &[], + ); let mut repository = - PlatformRepository::new4(vec![], IndexMap::new(), Some(Box::new(runtime)), None) - .unwrap(); + PlatformRepository::new(vec![], IndexMap::new(), Some(platform_info), None).unwrap(); for (package_name, version) in packages { let package = repository @@ -198,22 +204,16 @@ fn test_php_version() { #[test] fn test_inet_pton_regression() { - let mut runtime = MockRuntime::new(); // PHP: ->expects(self::once())->method('invoke')->with('inet_pton', ['::'])->willReturn(false). - runtime - .expect_invoke() - .with( - eq(PhpMixed::String("inet_pton".to_string())), - eq(vec![PhpMixed::String("::".to_string())]), - ) - .times(1) - .returning(|_callable, _arguments| PhpMixed::Bool(false)); - // suppressing PHP_ZTS & AF_INET6 - runtime - .expect_has_constant() - .times(..) - .returning(|_, _| false); + // TODO(phase-d): the payload reports the result of `@inet_pton('::')` instead of answering a + // call, so there is nothing left for the once() call-count check to observe. + let functions = [( + PhpMixed::String("inet_pton".to_string()), + vec![PhpMixed::String("::".to_string())], + PhpMixed::Bool(false), + )]; + // suppressing PHP_ZTS & AF_INET6 by leaving them undefined let constants: IndexMap<String, PhpMixed> = IndexMap::from([ ( "PHP_VERSION".to_string(), @@ -221,22 +221,21 @@ fn test_inet_pton_regression() { ), ("PHP_DEBUG".to_string(), PhpMixed::Bool(false)), ]); - runtime - .expect_get_constant() - .times(..) - .returning(move |constant, class| { - constants - .get(&constant_key(constant, class.as_deref())) - .cloned() - .unwrap_or(PhpMixed::Null) - }); - runtime - .expect_get_extensions() - .times(..) - .returning(Vec::new); + + let platform_info = platform_info( + constants + .into_iter() + .map(|(constant_name, value)| (constant_name, None, value)) + .collect(), + Vec::new(), + "", + None, + &functions, + &[], + ); let mut repository = - PlatformRepository::new4(vec![], IndexMap::new(), Some(Box::new(runtime)), None).unwrap(); + PlatformRepository::new(vec![], IndexMap::new(), Some(platform_info), None).unwrap(); let package = repository .find_package("php-ipv6", FindPackageConstraint::String("*".to_string())) .unwrap(); @@ -1630,79 +1629,17 @@ fn test_library_information() { PhpMixed::String("7.1.0".to_string()), )); - let functions = case.functions.clone(); - let info = case.info.map(|s| s.to_string()); - - let exts_for_get = extensions.clone(); - let constants_has = constants.clone(); - let constants_get = constants.clone(); - - let mut runtime = MockRuntime::new(); - runtime - .expect_get_extensions() - .times(..) - .returning(move || exts_for_get.clone()); - runtime - .expect_get_extension_version() - .times(..) - .returning(move |_extension| extension_version.to_string()); - runtime - .expect_get_extension_info() - .times(..) - .returning(move |_extension| Ok(info.clone().unwrap_or_default())); - runtime - .expect_invoke() - .times(..) - .returning(move |callable, arguments| { - for (c, a, ret) in &functions { - if *c == callable && *a == arguments { - return ret.clone(); - } - } - PhpMixed::Null - }); - runtime - .expect_has_constant() - .times(..) - .returning(move |constant, class| { - constants_has - .iter() - .any(|(n, c, _)| n == constant && c.as_deref() == class.as_deref()) - }); - runtime - .expect_get_constant() - .times(..) - .returning(move |constant, class| { - constants_get - .iter() - .find(|(n, c, _)| n == constant && c.as_deref() == class.as_deref()) - .map(|(_, _, v)| v.clone()) - .unwrap_or(PhpMixed::Null) - }); - let class_definitions_has = case.class_definitions.clone(); - let class_definitions_construct = case.class_definitions.clone(); - runtime - .expect_has_class() - .times(..) - .returning(move |class| class_definitions_has.iter().any(|d| d.class == class)); - runtime - .expect_construct() - .times(..) - .returning(move |class, arguments| { - for d in &class_definitions_construct { - if d.class == class - && let Some((args, ret)) = &d.construct - && *args == arguments - { - return Ok(ret.clone()); - } - } - Ok(PhpMixed::Null) - }); + let platform_info = platform_info( + constants, + extensions.clone(), + extension_version, + case.info, + &case.functions, + &case.class_definitions, + ); let mut platform_repository = - PlatformRepository::new4(vec![], IndexMap::new(), Some(Box::new(runtime)), None) - .unwrap(); + PlatformRepository::new(vec![], IndexMap::new(), Some(platform_info), None).unwrap(); let libraries: Vec<String> = platform_repository .search("lib".to_string(), SEARCH_NAME, None) @@ -1794,33 +1731,22 @@ fn test_composer_platform_version() { ("PHP_DEBUG".to_string(), PhpMixed::Bool(false)), ]); - let mut runtime = MockRuntime::new(); - runtime - .expect_get_extensions() - .times(..) - .returning(Vec::new); - runtime - .expect_get_constant() - .times(..) - .returning(move |constant, class| { - constants - .get(&constant_key(constant, class.as_deref())) - .cloned() - .unwrap_or(PhpMixed::Null) - }); // PHP only stubs getExtensions/getConstant; PHPUnit auto-returns null/false for the - // other probed methods. Mirror that so initialize() does not hit unset expectations. - runtime - .expect_has_constant() - .times(..) - .returning(|_, _| false); - runtime - .expect_invoke() - .times(..) - .returning(|_, _| PhpMixed::Null); + // other methods, which is what the default payload reports. + let platform_info = platform_info( + constants + .into_iter() + .map(|(constant_name, value)| (constant_name, None, value)) + .collect(), + Vec::new(), + "", + None, + &[], + &[], + ); let mut platform_repository = - PlatformRepository::new4(vec![], IndexMap::new(), Some(Box::new(runtime)), None).unwrap(); + PlatformRepository::new(vec![], IndexMap::new(), Some(platform_info), None).unwrap(); let package = platform_repository .find_package( diff --git a/docs/dev/php-rpc.md b/docs/dev/php-rpc.md index 392b318e..c833bbea 100644 --- a/docs/dev/php-rpc.md +++ b/docs/dev/php-rpc.md @@ -5,9 +5,7 @@ and its plugin/script machinery executes real PHP code. To mimic this behavior n runtime. The `shirabe-php-rpc` crate spawns the system PHP as a child process and talks to it over a Unix -domain socket. There is exactly one child process per Shirabe process, shared by every caller; -it hosts both the simple runtime queries (`get_php_version`, `has_constant`, ...) and the plugin -protocol. +domain socket. There is exactly one child process per Shirabe process, shared by every caller. ## Locating PHP @@ -129,9 +127,17 @@ same dispatch while waiting for its own `Return`. ## Worker dispatch table -`CallFunction` first consults the worker's dispatch table (composite queries like `diagnose`, -Shirabe-internal helpers prefixed `__shirabe_`), then falls back to calling the named PHP -function; an unknown name is an explicit error. Notable internal helpers: +`CallFunction` first consults the worker's dispatch table (composite queries like `diagnose` and +`platform`, Shirabe-internal helpers prefixed `__shirabe_`), then falls back to calling the named +PHP function; an unknown name is an explicit error. + +A composite query answers everything one consumer needs about the runtime in a single round trip, +because asking one constant and one extension at a time costs a round trip each. The Rust side +decodes the answer into a struct cached in a `OnceLock` (`Diagnostics` for `diagnose`, +`PlatformInfo` for `platform`) whose accessors panic on a name the worker does not report, so a +consumer and the worker cannot drift apart unnoticed. + +Notable internal helpers: - `__shirabe_eval` — runs a Rust-generated PHP snippet and returns its `return` value (used by the `scripts` Command-class execution path and the `_composer_tmp` class-rename path of |
