aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe-php-rpc
diff options
context:
space:
mode:
Diffstat (limited to 'crates/shirabe-php-rpc')
-rw-r--r--crates/shirabe-php-rpc/php/worker.php134
-rw-r--r--crates/shirabe-php-rpc/src/lib.rs522
2 files changed, 516 insertions, 140 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);
+ }
}