From 18a37a098157a98edbc8473e9c0d8dff3e8a88fa Mon Sep 17 00:00:00 2001 From: nsfisis Date: Fri, 7 Aug 2026 20:51:59 +0900 Subject: fix(plugin): make proxy stub property access an explicit error MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A proxy stub declares none of the real class's instance properties, and the `__get`/`__set` forwarders were emitted only for classes that declare a public one. Every other property access therefore got PHP's own answer for an undeclared property — null on a read, a dynamic property on a write — so plugin code reading state the entity holds ran on with null and failed somewhere else entirely, or not at all. Emit the forwarders on every root stub, add `__isset`/`__unset` alongside them so `isset()` cannot answer false silently either, and serve all four from one dispatcher that answers the state the Rust-side entity exposes and rejects every other name. Co-Authored-By: Claude Opus 5 (1M context) --- .../shirabe-php-rpc/php/stubs/Composer/Config.php | 20 ++++++ .../stubs/Composer/Downloader/DownloadManager.php | 20 ++++++ .../Composer/EventDispatcher/EventDispatcher.php | 20 ++++++ .../php/stubs/Composer/IO/BaseIO.php | 20 ++++++ .../Composer/Installer/InstallationManager.php | 20 ++++++ .../php/stubs/Composer/Package/BasePackage.php | 31 +++++---- .../php/stubs/Composer/PartialComposer.php | 20 ++++++ .../stubs/Composer/Repository/ArrayRepository.php | 20 ++++++ .../Composer/Repository/RepositoryManager.php | 20 ++++++ crates/shirabe/src/plugin/php_plugin_proxy.rs | 73 ++++++++++++++-------- 10 files changed, 227 insertions(+), 37 deletions(-) (limited to 'crates') diff --git a/crates/shirabe-php-rpc/php/stubs/Composer/Config.php b/crates/shirabe-php-rpc/php/stubs/Composer/Config.php index bccb8dcb..2424e5e3 100644 --- a/crates/shirabe-php-rpc/php/stubs/Composer/Config.php +++ b/crates/shirabe-php-rpc/php/stubs/Composer/Config.php @@ -51,6 +51,26 @@ class Config implements \ShirabeRustStub \ShirabeRustObjectRegistry::adopt($this->__rhandle, $this); } + public function __get($name) + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, '__get', [$name]); + } + + public function __set($name, $value): void + { + \ShirabeRpcRuntime::callRust($this->__rhandle, '__set', [$name, $value]); + } + + public function __isset($name): bool + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, '__isset', [$name]); + } + + public function __unset($name): void + { + \ShirabeRpcRuntime::callRust($this->__rhandle, '__unset', [$name]); + } + public function __construct(bool $useEnvironment = true, ?string $baseDir = null) { [$this->__rhandle, $this->__epoch] = \ShirabeRpcRuntime::callRust(0, '__shirabeConstruct', [static::class, [$useEnvironment, $baseDir]]); diff --git a/crates/shirabe-php-rpc/php/stubs/Composer/Downloader/DownloadManager.php b/crates/shirabe-php-rpc/php/stubs/Composer/Downloader/DownloadManager.php index 3828ac4a..198bb4d0 100644 --- a/crates/shirabe-php-rpc/php/stubs/Composer/Downloader/DownloadManager.php +++ b/crates/shirabe-php-rpc/php/stubs/Composer/Downloader/DownloadManager.php @@ -51,6 +51,26 @@ class DownloadManager implements \ShirabeRustStub \ShirabeRustObjectRegistry::adopt($this->__rhandle, $this); } + public function __get($name) + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, '__get', [$name]); + } + + public function __set($name, $value): void + { + \ShirabeRpcRuntime::callRust($this->__rhandle, '__set', [$name, $value]); + } + + public function __isset($name): bool + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, '__isset', [$name]); + } + + public function __unset($name): void + { + \ShirabeRpcRuntime::callRust($this->__rhandle, '__unset', [$name]); + } + public function __construct(IOInterface $io, bool $preferSource = false, ?Filesystem $filesystem = null) { [$this->__rhandle, $this->__epoch] = \ShirabeRpcRuntime::callRust(0, '__shirabeConstruct', [static::class, [$io, $preferSource, $filesystem]]); diff --git a/crates/shirabe-php-rpc/php/stubs/Composer/EventDispatcher/EventDispatcher.php b/crates/shirabe-php-rpc/php/stubs/Composer/EventDispatcher/EventDispatcher.php index e2369ed2..be9e8f8d 100644 --- a/crates/shirabe-php-rpc/php/stubs/Composer/EventDispatcher/EventDispatcher.php +++ b/crates/shirabe-php-rpc/php/stubs/Composer/EventDispatcher/EventDispatcher.php @@ -53,6 +53,26 @@ class EventDispatcher implements \ShirabeRustStub \ShirabeRustObjectRegistry::adopt($this->__rhandle, $this); } + public function __get($name) + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, '__get', [$name]); + } + + public function __set($name, $value): void + { + \ShirabeRpcRuntime::callRust($this->__rhandle, '__set', [$name, $value]); + } + + public function __isset($name): bool + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, '__isset', [$name]); + } + + public function __unset($name): void + { + \ShirabeRpcRuntime::callRust($this->__rhandle, '__unset', [$name]); + } + public function __construct(PartialComposer $composer, IOInterface $io, ?ProcessExecutor $process = null) { [$this->__rhandle, $this->__epoch] = \ShirabeRpcRuntime::callRust(0, '__shirabeConstruct', [static::class, [$composer, $io, $process]]); diff --git a/crates/shirabe-php-rpc/php/stubs/Composer/IO/BaseIO.php b/crates/shirabe-php-rpc/php/stubs/Composer/IO/BaseIO.php index ecb4db2f..e32e8ee3 100644 --- a/crates/shirabe-php-rpc/php/stubs/Composer/IO/BaseIO.php +++ b/crates/shirabe-php-rpc/php/stubs/Composer/IO/BaseIO.php @@ -48,6 +48,26 @@ abstract class BaseIO implements IOInterface, \ShirabeRustStub \ShirabeRustObjectRegistry::adopt($this->__rhandle, $this); } + public function __get($name) + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, '__get', [$name]); + } + + public function __set($name, $value): void + { + \ShirabeRpcRuntime::callRust($this->__rhandle, '__set', [$name, $value]); + } + + public function __isset($name): bool + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, '__isset', [$name]); + } + + public function __unset($name): void + { + \ShirabeRpcRuntime::callRust($this->__rhandle, '__unset', [$name]); + } + public function __construct() { [$this->__rhandle, $this->__epoch] = \ShirabeRpcRuntime::callRust(0, '__shirabeConstruct', [static::class, []]); diff --git a/crates/shirabe-php-rpc/php/stubs/Composer/Installer/InstallationManager.php b/crates/shirabe-php-rpc/php/stubs/Composer/Installer/InstallationManager.php index 7f41f6b9..6d3e9cf9 100644 --- a/crates/shirabe-php-rpc/php/stubs/Composer/Installer/InstallationManager.php +++ b/crates/shirabe-php-rpc/php/stubs/Composer/Installer/InstallationManager.php @@ -58,6 +58,26 @@ class InstallationManager implements \ShirabeRustStub \ShirabeRustObjectRegistry::adopt($this->__rhandle, $this); } + public function __get($name) + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, '__get', [$name]); + } + + public function __set($name, $value): void + { + \ShirabeRpcRuntime::callRust($this->__rhandle, '__set', [$name, $value]); + } + + public function __isset($name): bool + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, '__isset', [$name]); + } + + public function __unset($name): void + { + \ShirabeRpcRuntime::callRust($this->__rhandle, '__unset', [$name]); + } + public function __construct(Loop $loop, IOInterface $io, ?EventDispatcher $eventDispatcher = null) { [$this->__rhandle, $this->__epoch] = \ShirabeRpcRuntime::callRust(0, '__shirabeConstruct', [static::class, [$loop, $io, $eventDispatcher]]); diff --git a/crates/shirabe-php-rpc/php/stubs/Composer/Package/BasePackage.php b/crates/shirabe-php-rpc/php/stubs/Composer/Package/BasePackage.php index 9fca286a..65eebd56 100644 --- a/crates/shirabe-php-rpc/php/stubs/Composer/Package/BasePackage.php +++ b/crates/shirabe-php-rpc/php/stubs/Composer/Package/BasePackage.php @@ -48,6 +48,26 @@ abstract class BasePackage implements PackageInterface, \ShirabeRustStub \ShirabeRustObjectRegistry::adopt($this->__rhandle, $this); } + public function __get($name) + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, '__get', [$name]); + } + + public function __set($name, $value): void + { + \ShirabeRpcRuntime::callRust($this->__rhandle, '__set', [$name, $value]); + } + + public function __isset($name): bool + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, '__isset', [$name]); + } + + public function __unset($name): void + { + \ShirabeRpcRuntime::callRust($this->__rhandle, '__unset', [$name]); + } + public function __construct(string $name) { [$this->__rhandle, $this->__epoch] = \ShirabeRpcRuntime::callRust(0, '__shirabeConstruct', [static::class, [$name]]); @@ -76,17 +96,6 @@ abstract class BasePackage implements PackageInterface, \ShirabeRustStub ]; public static $stabilities = self::STABILITIES; - /** The real class declares public properties; every access forwards to the entity. */ - public function __get($name) - { - return \ShirabeRpcRuntime::callRust($this->__rhandle, '__get', [$name]); - } - - public function __set($name, $value): void - { - \ShirabeRpcRuntime::callRust($this->__rhandle, '__set', [$name, $value]); - } - public static function packageNameToRegexp(string $allowPattern, string $wrap = '{^%s$}i'): string { $cleanedAllowPattern = str_replace('\\*', '.*', preg_quote($allowPattern)); diff --git a/crates/shirabe-php-rpc/php/stubs/Composer/PartialComposer.php b/crates/shirabe-php-rpc/php/stubs/Composer/PartialComposer.php index a48133cb..6f90113a 100644 --- a/crates/shirabe-php-rpc/php/stubs/Composer/PartialComposer.php +++ b/crates/shirabe-php-rpc/php/stubs/Composer/PartialComposer.php @@ -52,6 +52,26 @@ class PartialComposer implements \ShirabeRustStub \ShirabeRustObjectRegistry::adopt($this->__rhandle, $this); } + public function __get($name) + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, '__get', [$name]); + } + + public function __set($name, $value): void + { + \ShirabeRpcRuntime::callRust($this->__rhandle, '__set', [$name, $value]); + } + + public function __isset($name): bool + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, '__isset', [$name]); + } + + public function __unset($name): void + { + \ShirabeRpcRuntime::callRust($this->__rhandle, '__unset', [$name]); + } + public function __construct() { [$this->__rhandle, $this->__epoch] = \ShirabeRpcRuntime::callRust(0, '__shirabeConstruct', [static::class, []]); diff --git a/crates/shirabe-php-rpc/php/stubs/Composer/Repository/ArrayRepository.php b/crates/shirabe-php-rpc/php/stubs/Composer/Repository/ArrayRepository.php index a5358ff2..8210031a 100644 --- a/crates/shirabe-php-rpc/php/stubs/Composer/Repository/ArrayRepository.php +++ b/crates/shirabe-php-rpc/php/stubs/Composer/Repository/ArrayRepository.php @@ -48,6 +48,26 @@ class ArrayRepository implements RepositoryInterface, \ShirabeRustStub \ShirabeRustObjectRegistry::adopt($this->__rhandle, $this); } + public function __get($name) + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, '__get', [$name]); + } + + public function __set($name, $value): void + { + \ShirabeRpcRuntime::callRust($this->__rhandle, '__set', [$name, $value]); + } + + public function __isset($name): bool + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, '__isset', [$name]); + } + + public function __unset($name): void + { + \ShirabeRpcRuntime::callRust($this->__rhandle, '__unset', [$name]); + } + public function __construct(array $packages = []) { [$this->__rhandle, $this->__epoch] = \ShirabeRpcRuntime::callRust(0, '__shirabeConstruct', [static::class, [$packages]]); diff --git a/crates/shirabe-php-rpc/php/stubs/Composer/Repository/RepositoryManager.php b/crates/shirabe-php-rpc/php/stubs/Composer/Repository/RepositoryManager.php index 9f3567d3..f2af01e5 100644 --- a/crates/shirabe-php-rpc/php/stubs/Composer/Repository/RepositoryManager.php +++ b/crates/shirabe-php-rpc/php/stubs/Composer/Repository/RepositoryManager.php @@ -53,6 +53,26 @@ class RepositoryManager implements \ShirabeRustStub \ShirabeRustObjectRegistry::adopt($this->__rhandle, $this); } + public function __get($name) + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, '__get', [$name]); + } + + public function __set($name, $value): void + { + \ShirabeRpcRuntime::callRust($this->__rhandle, '__set', [$name, $value]); + } + + public function __isset($name): bool + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, '__isset', [$name]); + } + + public function __unset($name): void + { + \ShirabeRpcRuntime::callRust($this->__rhandle, '__unset', [$name]); + } + public function __construct(IOInterface $io, Config $config, HttpDownloader $httpDownloader, ?EventDispatcher $eventDispatcher = null, ?ProcessExecutor $process = null) { [$this->__rhandle, $this->__epoch] = \ShirabeRpcRuntime::callRust(0, '__shirabeConstruct', [static::class, [$io, $config, $httpDownloader, $eventDispatcher, $process]]); diff --git a/crates/shirabe/src/plugin/php_plugin_proxy.rs b/crates/shirabe/src/plugin/php_plugin_proxy.rs index bd40b358..541718af 100644 --- a/crates/shirabe/src/plugin/php_plugin_proxy.rs +++ b/crates/shirabe/src/plugin/php_plugin_proxy.rs @@ -305,6 +305,12 @@ impl RustMethodDispatcher for PluginRpcDispatcher<'_> { None => Err(runtime_throw(format!("unknown Rust handle {rhandle}"))), }; } + if matches!(method_name, "__get" | "__set" | "__isset" | "__unset") { + return match entity { + Some(entity) => dispatch_property_access(&entity, method_name, &args), + None => Err(runtime_throw(format!("unknown Rust handle {rhandle}"))), + }; + } match entity { Some(RustEntity::Io(io)) => dispatch_io_method(&io, method_name, &args), Some(RustEntity::Composer(composer)) => { @@ -458,6 +464,47 @@ fn clone_entity(entity: &RustEntity) -> Result { ])) } +/// Serves the `__get`/`__set`/`__isset`/`__unset` forwarders every proxy stub carries. +fn dispatch_property_access( + entity: &RustEntity, + method_name: &str, + args: &[PluginValue], +) -> Result { + let property = required_string_arg(method_name, args.first())?; + // `BasePackage::$id` is the one instance property the entities expose so far. + if let RustEntity::Package(package) = entity + && property == "id" + { + return match method_name { + "__get" => Ok(PluginValue::Int( + package.borrow().as_package_interface().get_id(), + )), + "__isset" => Ok(PluginValue::Bool(true)), + "__set" => { + let id = match args.get(1) { + Some(PluginValue::Int(id)) => *id, + other => { + return Err(runtime_throw(format!( + "the package property `id` takes an int, got {other:?}" + ))); + } + }; + package.borrow_mut().as_package_interface_mut().set_id(id); + Ok(PluginValue::Null) + } + _ => Err(runtime_throw( + "the package property `id` cannot be unset over RPC".to_string(), + )), + }; + } + // TODO(plugin): the instance properties the proxied classes expose are widened on demand, + // driven by these explicit errors from real plugins. Each one has to decide how the state + // the real class keeps in that property is served from the Rust-side entity. + Err(runtime_throw(format!( + "the property `{property}` is not available over RPC yet" + ))) +} + fn dispatch_composer_method( composer: &ComposerHandle, method_name: &str, @@ -1552,32 +1599,6 @@ fn dispatch_package_method( } return Ok(PluginValue::Null); } - // `BasePackage::$id` is the one public property of the package classes, so the stub's - // property forwarders only ever carry it. - "__get" | "__set" => { - let property = required_string_arg(method_name, args.first())?; - if property != "id" { - return Err(runtime_throw(format!( - "the package property `{property}` is not available over RPC" - ))); - } - return if method_name == "__get" { - Ok(PluginValue::Int( - package.borrow().as_package_interface().get_id(), - )) - } else { - let id = match args.get(1) { - Some(PluginValue::Int(id)) => *id, - other => { - return Err(runtime_throw(format!( - "the package property `id` takes an int, got {other:?}" - ))); - } - }; - package.borrow_mut().as_package_interface_mut().set_id(id); - Ok(PluginValue::Null) - }; - } "equals" => { let other = package_from_arg(method_name, args.first())?; let this = PackageInterfaceHandle::from_rc_unchecked(package.clone()); -- cgit v1.3.1