diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-08-16 14:56:45 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-08-16 14:56:45 +0900 |
| commit | 2faccc227b65ab9dea0f80cd008cbf22ca4c5142 (patch) | |
| tree | 6f4e11bc37489550d68ad57d210668d0bea9cfb6 | |
| parent | aa2124fe5d0c96078c034a6e4044b7e81acdf692 (diff) | |
| download | php-shirabe-2faccc227b65ab9dea0f80cd008cbf22ca4c5142.tar.gz php-shirabe-2faccc227b65ab9dea0f80cd008cbf22ca4c5142.tar.zst php-shirabe-2faccc227b65ab9dea0f80cd008cbf22ca4c5142.zip | |
feat(plugin): proxy Composer\Util\Filesystem into the plugin worker
The class was shadowed by a guard, so a plugin doing `new Filesystem()`
got an explicit error. It is classified rust-proxy and plugin-constructible
and the Rust port is complete, so listing it as a stub target and answering
its public surface from the entity is all it takes.
The constructor rejects a caller-supplied ProcessExecutor: that class has no
proxy stub, so the argument could only be a second instance the Rust side
never sees. findShortestPath re-checks its arguments at the boundary because
the port panics where PHP throws, and a panic would take the process down
instead of reaching the plugin's catch block.
phpstan/extension-installer matches upstream Composer byte for byte again.
| -rw-r--r-- | crates/shirabe-php-rpc/php/guards/Composer/Util/Filesystem.php | 167 | ||||
| -rw-r--r-- | crates/shirabe-php-rpc/php/stubs/Composer/Util/Filesystem.php | 243 | ||||
| -rw-r--r-- | crates/shirabe-php-rpc/src/lib.rs | 4 | ||||
| -rw-r--r-- | crates/shirabe/src/plugin/php_plugin_proxy.rs | 174 | ||||
| -rw-r--r-- | crates/shirabe/tests/plugin/e2e_extension_installer_test.rs | 4 | ||||
| -rw-r--r-- | scripts/plugin-stub-generator/targets.list | 1 |
6 files changed, 422 insertions, 171 deletions
diff --git a/crates/shirabe-php-rpc/php/guards/Composer/Util/Filesystem.php b/crates/shirabe-php-rpc/php/guards/Composer/Util/Filesystem.php deleted file mode 100644 index 6d8dbb24..00000000 --- a/crates/shirabe-php-rpc/php/guards/Composer/Util/Filesystem.php +++ /dev/null @@ -1,167 +0,0 @@ -<?php - -// Generated by scripts/plugin-stub-generator; do not edit by hand. -// Guard for Composer\Util\Filesystem. -// The Rust side owns this class and the worker has no proxy for it, so this -// declaration shadows the real one: the constants and the hierarchy stay, while -// constructing it or calling anything on it raises an explicit error. - -namespace Composer\Util; - -class Filesystem -{ - public function __construct(?ProcessExecutor $executor = null) - { - \ShirabeUnsupportedClass::fail(self::class, '__construct'); - } - - public function remove(string $file) - { - \ShirabeUnsupportedClass::fail(self::class, 'remove'); - } - - public function isDirEmpty(string $dir) - { - \ShirabeUnsupportedClass::fail(self::class, 'isDirEmpty'); - } - - public function emptyDirectory(string $dir, bool $ensureDirectoryExists = true) - { - \ShirabeUnsupportedClass::fail(self::class, 'emptyDirectory'); - } - - public function removeDirectory(string $directory) - { - \ShirabeUnsupportedClass::fail(self::class, 'removeDirectory'); - } - - public function removeDirectoryAsync(string $directory) - { - \ShirabeUnsupportedClass::fail(self::class, 'removeDirectoryAsync'); - } - - public function removeDirectoryPhp(string $directory) - { - \ShirabeUnsupportedClass::fail(self::class, 'removeDirectoryPhp'); - } - - public function ensureDirectoryExists(string $directory) - { - \ShirabeUnsupportedClass::fail(self::class, 'ensureDirectoryExists'); - } - - public function unlink(string $path) - { - \ShirabeUnsupportedClass::fail(self::class, 'unlink'); - } - - public function rmdir(string $path) - { - \ShirabeUnsupportedClass::fail(self::class, 'rmdir'); - } - - public function copyThenRemove(string $source, string $target) - { - \ShirabeUnsupportedClass::fail(self::class, 'copyThenRemove'); - } - - public function copy(string $source, string $target) - { - \ShirabeUnsupportedClass::fail(self::class, 'copy'); - } - - public function rename(string $source, string $target) - { - \ShirabeUnsupportedClass::fail(self::class, 'rename'); - } - - public function findShortestPath(string $from, string $to, bool $directories = false, bool $preferRelative = false) - { - \ShirabeUnsupportedClass::fail(self::class, 'findShortestPath'); - } - - public function findShortestPathCode(string $from, string $to, bool $directories = false, bool $staticCode = false, bool $preferRelative = false) - { - \ShirabeUnsupportedClass::fail(self::class, 'findShortestPathCode'); - } - - public function isAbsolutePath(string $path) - { - \ShirabeUnsupportedClass::fail(self::class, 'isAbsolutePath'); - } - - public function size(string $path) - { - \ShirabeUnsupportedClass::fail(self::class, 'size'); - } - - public function normalizePath(string $path) - { - \ShirabeUnsupportedClass::fail(self::class, 'normalizePath'); - } - - public static function trimTrailingSlash(string $path) - { - \ShirabeUnsupportedClass::fail(self::class, 'trimTrailingSlash'); - } - - public static function isLocalPath(string $path) - { - \ShirabeUnsupportedClass::fail(self::class, 'isLocalPath'); - } - - public static function getPlatformPath(string $path) - { - \ShirabeUnsupportedClass::fail(self::class, 'getPlatformPath'); - } - - public static function isReadable(string $path) - { - \ShirabeUnsupportedClass::fail(self::class, 'isReadable'); - } - - protected function directorySize(string $directory) - { - \ShirabeUnsupportedClass::fail(self::class, 'directorySize'); - } - - protected function getProcess() - { - \ShirabeUnsupportedClass::fail(self::class, 'getProcess'); - } - - public function relativeSymlink(string $target, string $link) - { - \ShirabeUnsupportedClass::fail(self::class, 'relativeSymlink'); - } - - public function isSymlinkedDirectory(string $directory) - { - \ShirabeUnsupportedClass::fail(self::class, 'isSymlinkedDirectory'); - } - - public function junction(string $target, string $junction) - { - \ShirabeUnsupportedClass::fail(self::class, 'junction'); - } - - public function isJunction(string $junction) - { - \ShirabeUnsupportedClass::fail(self::class, 'isJunction'); - } - - public function removeJunction(string $junction) - { - \ShirabeUnsupportedClass::fail(self::class, 'removeJunction'); - } - - public function filePutContentsIfModified(string $path, string $content) - { - \ShirabeUnsupportedClass::fail(self::class, 'filePutContentsIfModified'); - } - - public function safeCopy(string $source, string $target): void - { - \ShirabeUnsupportedClass::fail(self::class, 'safeCopy'); - } -} diff --git a/crates/shirabe-php-rpc/php/stubs/Composer/Util/Filesystem.php b/crates/shirabe-php-rpc/php/stubs/Composer/Util/Filesystem.php new file mode 100644 index 00000000..e1532889 --- /dev/null +++ b/crates/shirabe-php-rpc/php/stubs/Composer/Util/Filesystem.php @@ -0,0 +1,243 @@ +<?php + +// Generated by scripts/plugin-stub-generator; do not edit by hand. +// Proxy stub for Composer\Util\Filesystem: the public surface forwards to the Rust-side entity over RPC. + +namespace Composer\Util; + +use Composer\Pcre\Preg; + +class Filesystem implements \ShirabeRustStub +{ + /** @var int */ + protected $__rhandle; + /** @var int */ + protected $__epoch; + + /** + * Binds a stub the registry built for an existing entity. Proxy instantiation bypasses + * the constructor, which belongs to plugin code building a new entity instead. + */ + public function __shirabeBind(int $rhandle, int $epoch): void + { + $this->__rhandle = $rhandle; + $this->__epoch = $epoch; + } + + public function __destruct() + { + \ShirabeRustObjectRegistry::release($this->__rhandle); + } + + public function __shirabeRustHandleDescriptor(): array + { + return [ + '__rhandle' => $this->__rhandle, + '__class' => static::class, + '__epoch' => $this->__epoch, + ]; + } + + public function __clone() + { + // PHP has already shallow-copied this stub, so both copies would point at one + // entity and release it twice. The Rust side clones the entity instead, applying + // whatever __clone semantics the real class defines, and this copy rebinds to the + // fresh handle. Entities without clone semantics answer with an explicit error. + [$this->__rhandle, $this->__epoch] = \ShirabeRpcRuntime::callRust($this->__rhandle, '__shirabeClone', []); + \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(?ProcessExecutor $executor = null) + { + [$this->__rhandle, $this->__epoch] = \ShirabeRpcRuntime::callRust(0, '__shirabeConstruct', [static::class, [$executor]]); + \ShirabeRustObjectRegistry::adopt($this->__rhandle, $this); + } + + public static function trimTrailingSlash(string $path) + { + if (!Preg::isMatch('{^[/\\\\]+$}', $path)) { + $path = rtrim($path, '/\\'); + } + + return $path; + } + + public static function isLocalPath(string $path) + { + // on windows, \\foo indicates network paths so we exclude those from local paths, however it is unsafe + // on linux as file:////foo (which would be a network path \\foo on windows) will resolve to /foo which could be a local path + if (Platform::isWindows()) { + return Preg::isMatch('{^(file://(?!//)|/(?!/)|/?[a-z]:[\\\\/]|\.\.[\\\\/]|[a-z0-9_.-]+[\\\\/])}i', $path); + } + + return Preg::isMatch('{^(file://|/|/?[a-z]:[\\\\/]|\.\.[\\\\/]|[a-z0-9_.-]+[\\\\/])}i', $path); + } + + public static function getPlatformPath(string $path) + { + if (Platform::isWindows()) { + $path = Preg::replace('{^(?:file:///([a-z]):?/)}i', 'file://$1:/', $path); + } + + return Preg::replace('{^file://}i', '', $path); + } + + public static function isReadable(string $path) + { + if (is_readable($path)) { + return true; + } + + if (is_file($path)) { + return false !== Silencer::call('file_get_contents', $path, false, null, 0, 1); + } + + if (is_dir($path)) { + return false !== Silencer::call('opendir', $path); + } + + // assume false otherwise + return false; + } + + public function remove(string $file) + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, 'remove', [$file]); + } + + public function isDirEmpty(string $dir) + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, 'isDirEmpty', [$dir]); + } + + public function emptyDirectory(string $dir, bool $ensureDirectoryExists = true) + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, 'emptyDirectory', [$dir, $ensureDirectoryExists]); + } + + public function removeDirectory(string $directory) + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, 'removeDirectory', [$directory]); + } + + public function removeDirectoryAsync(string $directory) + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, 'removeDirectoryAsync', [$directory]); + } + + public function removeDirectoryPhp(string $directory) + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, 'removeDirectoryPhp', [$directory]); + } + + public function ensureDirectoryExists(string $directory) + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, 'ensureDirectoryExists', [$directory]); + } + + public function unlink(string $path) + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, 'unlink', [$path]); + } + + public function rmdir(string $path) + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, 'rmdir', [$path]); + } + + public function copyThenRemove(string $source, string $target) + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, 'copyThenRemove', [$source, $target]); + } + + public function copy(string $source, string $target) + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, 'copy', [$source, $target]); + } + + public function rename(string $source, string $target) + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, 'rename', [$source, $target]); + } + + public function findShortestPath(string $from, string $to, bool $directories = false, bool $preferRelative = false) + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, 'findShortestPath', [$from, $to, $directories, $preferRelative]); + } + + public function findShortestPathCode(string $from, string $to, bool $directories = false, bool $staticCode = false, bool $preferRelative = false) + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, 'findShortestPathCode', [$from, $to, $directories, $staticCode, $preferRelative]); + } + + public function isAbsolutePath(string $path) + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, 'isAbsolutePath', [$path]); + } + + public function size(string $path) + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, 'size', [$path]); + } + + public function normalizePath(string $path) + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, 'normalizePath', [$path]); + } + + public function relativeSymlink(string $target, string $link) + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, 'relativeSymlink', [$target, $link]); + } + + public function isSymlinkedDirectory(string $directory) + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, 'isSymlinkedDirectory', [$directory]); + } + + public function junction(string $target, string $junction) + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, 'junction', [$target, $junction]); + } + + public function isJunction(string $junction) + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, 'isJunction', [$junction]); + } + + public function removeJunction(string $junction) + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, 'removeJunction', [$junction]); + } + + public function filePutContentsIfModified(string $path, string $content) + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, 'filePutContentsIfModified', [$path, $content]); + } + + public function safeCopy(string $source, string $target): void + { + \ShirabeRpcRuntime::callRust($this->__rhandle, 'safeCopy', [$source, $target]); + } +} diff --git a/crates/shirabe-php-rpc/src/lib.rs b/crates/shirabe-php-rpc/src/lib.rs index 47304ba8..b2bc2c28 100644 --- a/crates/shirabe-php-rpc/src/lib.rs +++ b/crates/shirabe-php-rpc/src/lib.rs @@ -1007,6 +1007,10 @@ const STUB_FILES: &[(&str, &str)] = &[ "../php/stubs/Composer/DependencyResolver/Operation/MarkAliasUninstalledOperation.php" ), ), + ( + "Composer/Util/Filesystem.php", + include_str!("../php/stubs/Composer/Util/Filesystem.php"), + ), ]; /// Hand-written worker-side classes (two-world implementations with behavior of their own, not diff --git a/crates/shirabe/src/plugin/php_plugin_proxy.rs b/crates/shirabe/src/plugin/php_plugin_proxy.rs index 967faa1b..643658e3 100644 --- a/crates/shirabe/src/plugin/php_plugin_proxy.rs +++ b/crates/shirabe/src/plugin/php_plugin_proxy.rs @@ -45,6 +45,7 @@ enum RustEntity { DownloadManager( std::rc::Rc<std::cell::RefCell<dyn crate::downloader::DownloadManagerInterface>>, ), + Filesystem(std::rc::Rc<std::cell::RefCell<crate::util::Filesystem>>), Io(std::rc::Rc<std::cell::RefCell<dyn IOInterface>>), InstallationManager(std::rc::Rc<std::cell::RefCell<dyn InstallationManagerInterface>>), RepositoryManager(std::rc::Rc<std::cell::RefCell<dyn RepositoryManagerInterface>>), @@ -66,6 +67,7 @@ fn entity_ptr_id(entity: &RustEntity) -> usize { } RustEntity::Config(config) => std::rc::Rc::as_ptr(config) as *const () as usize, RustEntity::DownloadManager(dm) => std::rc::Rc::as_ptr(dm) as *const () as usize, + RustEntity::Filesystem(fs) => std::rc::Rc::as_ptr(fs) as *const () as usize, RustEntity::Io(io) => std::rc::Rc::as_ptr(io) as *const () as usize, RustEntity::InstallationManager(im) => std::rc::Rc::as_ptr(im) as *const () as usize, RustEntity::RepositoryManager(rm) => std::rc::Rc::as_ptr(rm) as *const () as usize, @@ -361,6 +363,7 @@ impl RustMethodDispatcher for PluginRpcDispatcher<'_> { Some(RustEntity::DownloadManager(dm)) => { dispatch_download_manager_method(&dm, method_name, &args) } + Some(RustEntity::Filesystem(fs)) => dispatch_filesystem_method(&fs, method_name, &args), Some(RustEntity::InstallationManager(im)) => { dispatch_installation_manager_method(&im, method_name, &args) } @@ -508,6 +511,25 @@ pub(crate) fn construct_entity(args: &[PluginValue]) -> Result<PluginValue, PhpT ), )); } + // A Filesystem shares no state with the object graph beyond the process executor it + // runs subprocesses through, so a plugin-built one is a complete instance rather than + // a second view on a Rust-side service. + "Composer\\Util\\Filesystem" => { + // TODO(plugin): `ProcessExecutor` has no proxy stub, so an executor argument could + // only be a second instance the Rust side never sees. + match ctor_args.first() { + None | Some(PluginValue::Null) => {} + other => { + return Err(runtime_throw(format!( + "{class} cannot take a ProcessExecutor over RPC yet, got {other:?}" + ))); + } + } + let rhandle = register_entity(RustEntity::Filesystem(std::rc::Rc::new( + std::cell::RefCell::new(crate::util::Filesystem::new(None)), + ))); + return Ok(construction_result(rhandle)); + } // TODO(plugin): the remaining proxied classes get a construction story on demand, // driven by explicit errors from real plugins. Each one has to decide what a // plugin-built instance means for the Rust-side graph, which is why none of them is @@ -551,6 +573,7 @@ fn clone_entity(entity: &RustEntity) -> Result<PluginValue, PhpThrow> { RustEntity::Composer(_) | RustEntity::Config(_) | RustEntity::DownloadManager(_) + | RustEntity::Filesystem(_) | RustEntity::Io(_) | RustEntity::InstallationManager(_) | RustEntity::RepositoryManager(_) @@ -902,6 +925,157 @@ fn resolved_promise(value: PluginValue) -> Result<PluginValue, PhpThrow> { } } +fn dispatch_filesystem_method( + fs: &std::rc::Rc<std::cell::RefCell<crate::util::Filesystem>>, + method_name: &str, + args: &[PluginValue], +) -> Result<PluginValue, PhpThrow> { + let string_arg = |position: usize| -> Result<String, PhpThrow> { + match args.get(position) { + // TODO(bytes): lossy UTF-8; paths are bytes in PHP. + Some(PluginValue::String(bytes)) => Ok(String::from_utf8_lossy(bytes).into_owned()), + other => Err(runtime_throw(format!( + "{method_name} expects a string argument at position {position}, got {other:?}" + ))), + } + }; + // TODO(plugin): the exception class the real method throws (RuntimeException, IOException, + // LogicException) is collapsed to RuntimeException on this side of the boundary. + let failed = |error: anyhow::Error| runtime_throw(format!("{method_name} failed: {error:#}")); + match method_name { + "remove" => Ok(PluginValue::Bool( + fs.borrow_mut().remove(string_arg(0)?).map_err(failed)?, + )), + "isDirEmpty" => Ok(PluginValue::Bool(fs.borrow().is_dir_empty(&string_arg(0)?))), + "emptyDirectory" => { + fs.borrow_mut() + .empty_directory(&string_arg(0)?, bool_arg(method_name, args.get(1))?) + .map_err(failed)?; + Ok(PluginValue::Null) + } + "removeDirectory" => Ok(PluginValue::Bool( + fs.borrow_mut() + .remove_directory(string_arg(0)?) + .map_err(failed)?, + )), + "removeDirectoryAsync" => { + let directory = string_arg(0)?; + let removed = crate::util::sync_executor::block_on(async { + crate::util::Filesystem::remove_directory_async_via(fs, &directory).await + }) + .map_err(failed)?; + resolved_promise(PluginValue::Bool(removed)) + } + "removeDirectoryPhp" => Ok(PluginValue::Bool( + fs.borrow_mut() + .remove_directory_php(&string_arg(0)?) + .map_err(failed)?, + )), + "ensureDirectoryExists" => { + fs.borrow_mut() + .ensure_directory_exists(&string_arg(0)?) + .map_err(failed)?; + Ok(PluginValue::Null) + } + "unlink" => Ok(PluginValue::Bool( + fs.borrow().unlink(string_arg(0)?).map_err(failed)?, + )), + "rmdir" => Ok(PluginValue::Bool( + fs.borrow().rmdir(string_arg(0)?).map_err(failed)?, + )), + "copyThenRemove" => { + fs.borrow_mut() + .copy_then_remove(&string_arg(0)?, &string_arg(1)?) + .map_err(failed)?; + Ok(PluginValue::Null) + } + "copy" => Ok(PluginValue::Bool( + fs.borrow_mut() + .copy(&string_arg(0)?, &string_arg(1)?) + .map_err(failed)?, + )), + "rename" => { + fs.borrow_mut() + .rename(string_arg(0)?, string_arg(1)?) + .map_err(failed)?; + Ok(PluginValue::Null) + } + "findShortestPath" | "findShortestPathCode" => { + let from = string_arg(0)?; + let to = string_arg(1)?; + // TODO(error-model): the port panics on a relative path where PHP throws + // InvalidArgumentException, and a panic would take the whole process down instead + // of reaching the plugin's catch block, so the check is repeated here. + let fs = fs.borrow(); + if !fs.is_absolute_path(&from) || !fs.is_absolute_path(&to) { + return Err(PhpThrow { + exception_class: "InvalidArgumentException".to_string(), + message: format!("$from ({from}) and $to ({to}) must be absolute paths."), + code: 0, + }); + } + Ok(PluginValue::string(if method_name == "findShortestPath" { + fs.find_shortest_path( + &from, + &to, + bool_arg(method_name, args.get(2))?, + bool_arg(method_name, args.get(3))?, + ) + } else { + fs.find_shortest_path_code( + &from, + &to, + bool_arg(method_name, args.get(2))?, + bool_arg(method_name, args.get(3))?, + bool_arg(method_name, args.get(4))?, + ) + })) + } + "isAbsolutePath" => Ok(PluginValue::Bool( + fs.borrow().is_absolute_path(&string_arg(0)?), + )), + "size" => Ok(PluginValue::Int( + fs.borrow().size(string_arg(0)?).map_err(failed)?, + )), + "normalizePath" => Ok(PluginValue::string( + fs.borrow().normalize_path(&string_arg(0)?), + )), + "relativeSymlink" => Ok(PluginValue::Bool( + fs.borrow() + .relative_symlink(&string_arg(0)?, &string_arg(1)?), + )), + "isSymlinkedDirectory" => Ok(PluginValue::Bool( + fs.borrow().is_symlinked_directory(&string_arg(0)?), + )), + "junction" => { + fs.borrow_mut() + .junction(&string_arg(0)?, &string_arg(1)?) + .map_err(failed)?; + Ok(PluginValue::Null) + } + "isJunction" => Ok(PluginValue::Bool(fs.borrow().is_junction(&string_arg(0)?))), + "removeJunction" => Ok(PluginValue::Bool( + fs.borrow_mut() + .remove_junction(&string_arg(0)?) + .map_err(failed)?, + )), + "filePutContentsIfModified" => Ok(PluginValue::Int( + fs.borrow() + .file_put_contents_if_modified(&string_arg(0)?, &string_arg(1)?) + .map_err(failed)?, + )), + "safeCopy" => { + fs.borrow() + .safe_copy(&string_arg(0)?, &string_arg(1)?) + .map_err(failed)?; + Ok(PluginValue::Null) + } + other => Err(runtime_throw(format!( + "the Filesystem method `{other}` is not available over RPC yet" + ))), + } +} + fn dispatch_event_dispatcher_method( dispatcher: &std::rc::Rc< std::cell::RefCell<dyn crate::event_dispatcher::EventDispatcherInterface>, diff --git a/crates/shirabe/tests/plugin/e2e_extension_installer_test.rs b/crates/shirabe/tests/plugin/e2e_extension_installer_test.rs index 0465757a..9a95888c 100644 --- a/crates/shirabe/tests/plugin/e2e_extension_installer_test.rs +++ b/crates/shirabe/tests/plugin/e2e_extension_installer_test.rs @@ -117,10 +117,6 @@ fn run_install(work: &Path, program: &str, args: &[&str]) -> InstallRun { } } -// TODO(plugin): the plugin's post-install listener does `new Composer\Util\Filesystem()`, and -// the guard class the worker loads for that FQCN raises an explicit error: the Rust side owns -// Filesystem and has no proxy for plugin-constructed instances of it. -#[ignore = "Filesystem is Rust-owned and has no proxy the plugin can construct; see the TODO(plugin) above"] #[test] fn test_extension_installer_install_matches_upstream_composer() { if !php_runtime_available() { diff --git a/scripts/plugin-stub-generator/targets.list b/scripts/plugin-stub-generator/targets.list index 411b9e3b..c013c3c2 100644 --- a/scripts/plugin-stub-generator/targets.list +++ b/scripts/plugin-stub-generator/targets.list @@ -33,3 +33,4 @@ Composer\DependencyResolver\Operation\UpdateOperation Composer\DependencyResolver\Operation\UninstallOperation Composer\DependencyResolver\Operation\MarkAliasInstalledOperation Composer\DependencyResolver\Operation\MarkAliasUninstalledOperation +Composer\Util\Filesystem |
