aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-08-16 14:56:45 +0900
committernsfisis <nsfisis@gmail.com>2026-08-16 14:56:45 +0900
commit2faccc227b65ab9dea0f80cd008cbf22ca4c5142 (patch)
tree6f4e11bc37489550d68ad57d210668d0bea9cfb6 /crates
parentaa2124fe5d0c96078c034a6e4044b7e81acdf692 (diff)
downloadphp-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.
Diffstat (limited to 'crates')
-rw-r--r--crates/shirabe-php-rpc/php/guards/Composer/Util/Filesystem.php167
-rw-r--r--crates/shirabe-php-rpc/php/stubs/Composer/Util/Filesystem.php243
-rw-r--r--crates/shirabe-php-rpc/src/lib.rs4
-rw-r--r--crates/shirabe/src/plugin/php_plugin_proxy.rs174
-rw-r--r--crates/shirabe/tests/plugin/e2e_extension_installer_test.rs4
5 files changed, 421 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() {