diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-08-09 09:25:37 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-08-09 09:25:37 +0900 |
| commit | d2a28f8c07b0aa713be338005c6153011d70f24b (patch) | |
| tree | 8424d8fe75326263bbb963d74611629df829006c | |
| parent | 6643eb8b7d305818f80c910144b3b29b1af34ba7 (diff) | |
| download | php-shirabe-d2a28f8c07b0aa713be338005c6153011d70f24b.tar.gz php-shirabe-d2a28f8c07b0aa713be338005c6153011d70f24b.tar.zst php-shirabe-d2a28f8c07b0aa713be338005c6153011d70f24b.zip | |
feat(symfony-filesystem): append the failure reason to IOExceptions
Symfony wraps its native filesystem calls in box(), which captures the
warning text of a failed call into self::$lastError and appends it to
the IOException message. The port dropped that text because the shim's
mkdir, rmdir, unlink and symlink only return PHP's bare false.
Give each of those a Result-returning variant carrying the io::Error,
and use it to restore the appended reason. That also makes two
conditions portable: doRemove()'s "Permission denied" test, and
linkException()'s Windows error code 1314 special case.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
| -rw-r--r-- | crates/shirabe-external-packages/src/symfony/filesystem/filesystem.rs | 106 | ||||
| -rw-r--r-- | crates/shirabe-php-shim/src/fs.rs | 33 |
2 files changed, 99 insertions, 40 deletions
diff --git a/crates/shirabe-external-packages/src/symfony/filesystem/filesystem.rs b/crates/shirabe-external-packages/src/symfony/filesystem/filesystem.rs index 295a7b86..450c7191 100644 --- a/crates/shirabe-external-packages/src/symfony/filesystem/filesystem.rs +++ b/crates/shirabe-external-packages/src/symfony/filesystem/filesystem.rs @@ -1,9 +1,9 @@ //! ref: composer/vendor/symfony/filesystem/Filesystem.php -// TODO(phase-c): PHP's box()/self::$lastError mechanism (captures the underlying OS error message -// from a failed native call via set_error_handler) is not modeled anywhere in this file. Every -// IOException message constructed below therefore omits the trailing low-level error string that -// PHP would append (e.g. "Failed to touch \"%s\": ".self::$lastError). +// TODO(phase-c): PHP's box()/self::$lastError mechanism captures the warning text emitted by the +// failed native call. The low-level error appended to the IOException messages below is instead the +// Rust io::Error text, which words the same errno differently (e.g. "Permission denied (os error +// 13)"). use crate::symfony::filesystem::exception::io_exception::IOException; use shirabe_php_shim::PhpMixed; @@ -146,9 +146,11 @@ impl Filesystem { continue; } - if !shirabe_php_shim::mkdir(&dir, mode, true) && !shirabe_php_shim::is_dir(&dir) { + if let Err(last_error) = shirabe_php_shim::mkdir_result(&dir, mode, true) + && !shirabe_php_shim::is_dir(&dir) + { return Err(IOException::new( - format!("Failed to create \"{}\": ", dir), + format!("Failed to create \"{}\": {}", dir, last_error), 0, None, Some(dir), @@ -200,13 +202,25 @@ impl Filesystem { for file in files { if shirabe_php_shim::is_link(&file) { // See https://bugs.php.net/52176 - if !(shirabe_php_shim::unlink(&file) - || !cfg!(windows) - || shirabe_php_shim::rmdir(&file)) - && shirabe_php_shim::file_exists(&file) - { + let unlinked = shirabe_php_shim::unlink_result(&file); + let mut last_error = unlinked.as_ref().err().map(ToString::to_string); + let mut removed = unlinked.is_ok() || !cfg!(windows); + if !removed { + match shirabe_php_shim::rmdir_result(&file) { + Ok(()) => { + last_error = None; + removed = true; + } + Err(e) => last_error = Some(e.to_string()), + } + } + if !removed && shirabe_php_shim::file_exists(&file) { return Err(IOException::new( - format!("Failed to remove symlink \"{}\": ", file), + format!( + "Failed to remove symlink \"{}\": {}", + file, + last_error.unwrap_or_default() + ), 0, None, None, @@ -237,27 +251,29 @@ impl Filesystem { (&entries).into_iter().map(|e| e.get_pathname()).collect(); Self::do_remove(child_paths, true)?; - if !shirabe_php_shim::rmdir(&file) && shirabe_php_shim::file_exists(&file) { + if let Err(last_error) = shirabe_php_shim::rmdir_result(&file) + && shirabe_php_shim::file_exists(&file) + { return Err(IOException::new( - format!("Failed to remove directory \"{}\": ", file), + format!("Failed to remove directory \"{}\": {}", file, last_error), + 0, + None, + None, + ) + .into()); + } + } else if let Err(last_error) = shirabe_php_shim::unlink_result(&file) { + let last_error = last_error.to_string(); + if last_error.contains("Permission denied") || shirabe_php_shim::file_exists(&file) + { + return Err(IOException::new( + format!("Failed to remove file \"{}\": {}", file, last_error), 0, None, None, ) .into()); } - } else if !shirabe_php_shim::unlink(&file) && shirabe_php_shim::file_exists(&file) { - // TODO(phase-c): PHP also throws when self::$lastError contains "Permission - // denied", even if file_exists() is now false (e.g. the file vanished between the - // failed unlink and this check). That OR-branch is dropped along with the general - // $lastError omission noted at the top of this file. - return Err(IOException::new( - format!("Failed to remove file \"{}\": ", file), - 0, - None, - None, - ) - .into()); } } Ok(()) @@ -293,21 +309,41 @@ impl Filesystem { self.remove(PhpMixed::String(target_dir.clone()))?; } - if !shirabe_php_shim::symlink(&origin_dir, &target_dir) { - return Self::link_exception(&origin_dir, &target_dir, "symbolic"); + if let Err(last_error) = shirabe_php_shim::symlink_result(&origin_dir, &target_dir) { + return Self::link_exception( + &origin_dir, + &target_dir, + "symbolic", + &last_error.to_string(), + ); } Ok(()) } - // TODO(phase-c): PHP special-cases a Windows error containing "error code(1314)" with a - // distinct "Do you have the required Administrator-rights?" message; that check (and the - // self::$lastError inspection it depends on) is not ported, so this always throws the generic - // message below. - fn link_exception(origin: &str, target: &str, link_type: &str) -> anyhow::Result<()> { + fn link_exception( + origin: &str, + target: &str, + link_type: &str, + last_error: &str, + ) -> anyhow::Result<()> { + // TODO(phase-c): the Windows io::Error text renders the error number as "(os error 1314)", + // so this substring never matches and the generic message below is thrown instead. + if !last_error.is_empty() && cfg!(windows) && last_error.contains("error code(1314)") { + return Err(IOException::new( + format!( + "Unable to create \"{}\" link due to error code 1314: 'A required privilege is not held by the client'. Do you have the required Administrator-rights?", + link_type + ), + 0, + None, + Some(target.to_string()), + ) + .into()); + } Err(IOException::new( format!( - "Failed to create \"{}\" link from \"{}\" to \"{}\": ", - link_type, origin, target + "Failed to create \"{}\" link from \"{}\" to \"{}\": {}", + link_type, origin, target, last_error ), 0, None, diff --git a/crates/shirabe-php-shim/src/fs.rs b/crates/shirabe-php-shim/src/fs.rs index b4010bb0..eae41dda 100644 --- a/crates/shirabe-php-shim/src/fs.rs +++ b/crates/shirabe-php-shim/src/fs.rs @@ -835,7 +835,11 @@ pub fn filemtime(_filename: impl AsRef<std::path::Path>) -> Option<i64> { } pub fn unlink(path: impl AsRef<std::path::Path>) -> bool { - std::fs::remove_file(path).is_ok() + unlink_result(path).is_ok() +} + +pub fn unlink_result(path: impl AsRef<std::path::Path>) -> Result<(), std::io::Error> { + std::fs::remove_file(path) } pub fn unlink_silent(_path: impl AsRef<std::path::Path>) -> bool { @@ -965,15 +969,27 @@ pub fn umask() -> u32 { } pub fn mkdir(_pathname: impl AsRef<std::path::Path>, _mode: u32, _recursive: bool) -> bool { + mkdir_result(_pathname, _mode, _recursive).is_ok() +} + +pub fn mkdir_result( + pathname: impl AsRef<std::path::Path>, + mode: u32, + recursive: bool, +) -> Result<(), std::io::Error> { use std::os::unix::fs::DirBuilderExt; // DirBuilder::mode passes the mode to mkdir(2), which applies the process umask, matching PHP. let mut builder = std::fs::DirBuilder::new(); - builder.mode(_mode).recursive(_recursive); - builder.create(_pathname.as_ref()).is_ok() + builder.mode(mode).recursive(recursive); + builder.create(pathname.as_ref()) } pub fn rmdir(dir: impl AsRef<std::path::Path>) -> bool { - std::fs::remove_dir(dir).is_ok() + rmdir_result(dir).is_ok() +} + +pub fn rmdir_result(dir: impl AsRef<std::path::Path>) -> Result<(), std::io::Error> { + std::fs::remove_dir(dir) } pub fn rename( @@ -1014,7 +1030,14 @@ pub fn ftruncate(stream: &PhpResource, size: i64) -> bool { } pub fn symlink(_target: impl AsRef<std::path::Path>, _link: impl AsRef<std::path::Path>) -> bool { - std::os::unix::fs::symlink(_target.as_ref(), _link.as_ref()).is_ok() + symlink_result(_target, _link).is_ok() +} + +pub fn symlink_result( + target: impl AsRef<std::path::Path>, + link: impl AsRef<std::path::Path>, +) -> Result<(), std::io::Error> { + std::os::unix::fs::symlink(target.as_ref(), link.as_ref()) } pub fn sys_get_temp_dir() -> String { |
